//
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//

import Foundation
import XCTest

@testable import SignalServiceKit

class TestJobRecord: SSKJobRecord {

}

let kJobRecordLabel = "TestJobRecord"
class TestJobQueue: JobQueue {

    // MARK: JobQueue

    typealias DurableOperationType = TestDurableOperation
    var jobRecordLabel: String = kJobRecordLabel
    static var maxRetries: UInt = 1
    var runningOperations: [TestDurableOperation] = []
    var requiresInternet: Bool = false

    func setup() {
        defaultSetup()
    }

    func didMarkAsReady(oldJobRecord: TestJobRecord, transaction: YapDatabaseReadWriteTransaction) {
        // no special handling
    }

    var isSetup: Bool = false

    let operationQueue = OperationQueue()

    func operationQueue(jobRecord: TestJobRecord) -> OperationQueue {
        return self.operationQueue
    }

    func buildOperation(jobRecord: TestJobRecord, transaction: YapDatabaseReadTransaction) throws -> TestDurableOperation {
        return TestDurableOperation(jobRecord: jobRecord, jobBlock: self.jobBlock)
    }

    // MARK: 

    var jobBlock: (JobRecordType) -> Void = { _ in /* noop */ }
    init() { }
}

class TestDurableOperation: OWSOperation, DurableOperation {

    // MARK: DurableOperation

    var jobRecord: TestJobRecord

    weak var durableOperationDelegate: TestJobQueue?

    var operation: OWSOperation {
        return self
    }

    // MARK: 

    var jobBlock: (TestJobRecord) -> Void

    init(jobRecord: TestJobRecord, jobBlock: @escaping (TestJobRecord) -> Void) {
        self.jobRecord = jobRecord
        self.jobBlock = jobBlock
    }

    override func run() {
        jobBlock(jobRecord)
        self.reportSuccess()
    }
}

class JobQueueTest: SSKBaseTestSwift {

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

    // MARK: 

    func buildJobRecord() -> TestJobRecord {
        return TestJobRecord(label: kJobRecordLabel)
    }

    // MARK: 

    func test_setupMarksInProgressJobsAsReady() {

        let dispatchGroup = DispatchGroup()

        let jobQueue = TestJobQueue()
        let jobRecord1 = buildJobRecord()
        let jobRecord2 = buildJobRecord()
        let jobRecord3 = buildJobRecord()

        var runList: [TestJobRecord] = []

        jobQueue.jobBlock = { jobRecord in
            runList.append(jobRecord)
            dispatchGroup.leave()
        }

        self.readWrite { transaction in
            jobQueue.add(jobRecord: jobRecord1, transaction: transaction)
            jobQueue.add(jobRecord: jobRecord2, transaction: transaction)
            jobQueue.add(jobRecord: jobRecord3, transaction: transaction)
        }
        dispatchGroup.enter()
        dispatchGroup.enter()
        dispatchGroup.enter()

        let finder = JobRecordFinder()
        self.readWrite { transaction in
            XCTAssertEqual(3, finder.allRecords(label: kJobRecordLabel, status: .ready, transaction: transaction).count)
        }

        // start queue
        jobQueue.setup()

        if case .timedOut = dispatchGroup.wait(timeout: .now() + 1.0) {
            XCTFail("timed out waiting for jobs")
        }

        // Normally an operation enqueued for a JobRecord by a JobQueue will mark itself as complete
        // by deleting itself.
        // For testing, the operations enqueued by the TestJobQueue do *not* delete themeselves upon
        // completion, simulating an operation which never compeleted.

        self.readWrite { transaction in
            XCTAssertEqual(0, finder.allRecords(label: kJobRecordLabel, status: .ready, transaction: transaction).count)
            XCTAssertEqual(3, finder.allRecords(label: kJobRecordLabel, status: .running, transaction: transaction).count)
        }

        // Verify re-queue
        jobQueue.isSetup = false
        jobQueue.setup()

        self.readWrite { transaction in
            XCTAssertEqual(3, finder.allRecords(label: kJobRecordLabel, status: .ready, transaction: transaction).count)
            XCTAssertEqual(0, finder.allRecords(label: kJobRecordLabel, status: .running, transaction: transaction).count)
        }

        let rerunGroup = DispatchGroup()
        rerunGroup.enter()
        rerunGroup.enter()
        rerunGroup.enter()

        var rerunList: [TestJobRecord] = []
        jobQueue.jobBlock = { jobRecord in
            rerunList.append(jobRecord)
            rerunGroup.leave()
        }

        jobQueue.isSetup = true

        switch rerunGroup.wait(timeout: .now() + 1.0) {
        case .timedOut:
            XCTFail("timed out waiting for retry")
        case .success:
            // verify order maintained on requeue
            XCTAssertEqual([jobRecord1, jobRecord2, jobRecord3].map { $0.uniqueId }, rerunList.map { $0.uniqueId })
        }
    }
}