//
//  AdvancedReOrgTests.swift
//  ZcashLightClientKit-Unit-Tests
//
//  Created by Francisco Gindre on 5/14/20.
//

import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit

// swiftlint:disable implicitly_unwrapped_optional force_unwrapping type_body_length
//@MainActor
class AdvancedReOrgTests: XCTestCase {
    // TODO: Parameterize this from environment?
    // swiftlint:disable:next line_length
    var seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"

    // TODO: Parameterize this from environment
    let testRecipientAddress = "zs17mg40levjezevuhdp5pqrd52zere7r7vrjgdwn5sj4xsqtm20euwahv9anxmwr3y3kmwuz8k55a"

    let sendAmount = Zatoshi(1000)
    var birthday: BlockHeight = 663150
    let defaultLatestHeight: BlockHeight = 663175
    var coordinator: TestCoordinator!
    var syncedExpectation = XCTestExpectation(description: "synced")
    var sentTransactionExpectation = XCTestExpectation(description: "sent")
    var expectedReorgHeight: BlockHeight = 665188
    var expectedRewindHeight: BlockHeight = 665188
    var reorgExpectation = XCTestExpectation(description: "reorg")
    let branchID = "2bb40e60"
    let chainName = "main"
    let network = DarksideWalletDNetwork()
    
    override func setUpWithError() throws {
        try super.setUpWithError()
        coordinator = try TestCoordinator(
            seed: seedPhrase,
            walletBirthday: birthday + 50, //don't use an exact birthday, users never do.
            channelProvider: ChannelProvider(),
            network: network
        )
        try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
    }
    
    override func tearDownWithError() throws {
        try super.tearDownWithError()
        NotificationCenter.default.removeObserver(self)
        try coordinator.stop()
        try? FileManager.default.removeItem(at: coordinator.databases.cacheDB)
        try? FileManager.default.removeItem(at: coordinator.databases.dataDB)
        try? FileManager.default.removeItem(at: coordinator.databases.pendingDB)
    }
    
    @objc func handleReorg(_ notification: Notification) {
        guard
            let reorgHeight = notification.userInfo?[CompactBlockProcessorNotificationKey.reorgHeight] as? BlockHeight,
            let rewindHeight = notification.userInfo?[CompactBlockProcessorNotificationKey.rewindHeight] as? BlockHeight
        else {
            XCTFail("empty reorg notification")
            return
        }

        logger!.debug("--- REORG DETECTED \(reorgHeight)--- RewindHeight: \(rewindHeight)", file: #file, function: #function, line: #line)

        XCTAssertEqual(reorgHeight, expectedReorgHeight)
        reorgExpectation.fulfill()
    }
    

    /// pre-condition: know balances before tx at received_Tx_height arrives
    /// 1. Setup w/ default dataset
    /// 2. applyStaged(received_Tx_height)
    /// 3. sync up to received_Tx_height
    /// 3a. verify that balance is previous balance + tx amount
    /// 4. get that transaction hex encoded data
    /// 5. stage 5 empty blocks w/heights received_Tx_height to received_Tx_height + 3
    /// 6. stage tx at received_Tx_height + 3
    /// 6a. applyheight(received_Tx_height + 1)
    /// 7. sync to received_Tx_height + 1
    /// 8. assert that reorg happened at received_Tx_height
    /// 9. verify that balance equals initial balance
    /// 10. sync up to received_Tx_height + 3
    /// 11. verify that balance equals initial balance + tx amount
    func testReOrgChangesInboundTxMinedHeight() throws {
        hookToReOrgNotification()
        try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
        var shouldContinue = false
        let receivedTxHeight: BlockHeight = 663188
        var initialTotalBalance = Zatoshi(-1)
        var initialVerifiedBalance = Zatoshi(-1)
        self.expectedReorgHeight = receivedTxHeight + 1
        
        /*
        precondition:know balances before tx at received_Tx_height arrives
        */
        try coordinator.applyStaged(blockheight: receivedTxHeight - 1)
        sleep(3)
        let preTxExpectation = XCTestExpectation(description: "pre receive")
        
        var synchronizer: SDKSynchronizer?
        
        try coordinator.sync(
            completion: { synchro in
                synchronizer = synchro
                initialVerifiedBalance = synchro.initializer.getVerifiedBalance()
                initialTotalBalance = synchro.initializer.getBalance()
                preTxExpectation.fulfill()
                shouldContinue = true
            },
            error: self.handleError
        )
        
        wait(for: [preTxExpectation], timeout: 10)
        
        guard shouldContinue else {
            XCTFail("pre receive sync failed")
            return
        }
        
        /*
        2. applyStaged(received_Tx_height)
        */
        try coordinator.applyStaged(blockheight: receivedTxHeight)
        sleep(2)
        
        /*
        3. sync up to received_Tx_height
        */
        let receivedTxExpectation = XCTestExpectation(description: "received tx")
        var receivedTxTotalBalance = Zatoshi(-1)
        var receivedTxVerifiedBalance = Zatoshi(-1)
        
        try coordinator.sync(completion: { synchro in
            synchronizer = synchro
            receivedTxVerifiedBalance = synchro.initializer.getVerifiedBalance()
            receivedTxTotalBalance = synchro.initializer.getBalance()
            receivedTxExpectation.fulfill()
        }, error: self.handleError)
        sleep(2)
        wait(for: [receivedTxExpectation], timeout: 10)
        
        guard let syncedSynchronizer = synchronizer else {
            XCTFail("nil synchronizer")
            return
        }
        sleep(5)
        guard let receivedTx = syncedSynchronizer.receivedTransactions.first, receivedTx.minedHeight == receivedTxHeight else {
            XCTFail("did not receive transaction")
            return
        }
        
        /*
        3a. verify that balance is previous balance + tx amount
        */
        XCTAssertEqual(receivedTxTotalBalance, initialTotalBalance + receivedTx.value)
        XCTAssertEqual(receivedTxVerifiedBalance, initialVerifiedBalance)

        /*
        4. get that transaction hex encoded data
        */
        guard let receivedTxData = receivedTx.raw else {
            XCTFail("received tx has no raw data!")
            return
        }
        
        let receivedRawTx = RawTransaction.with { rawTx in
            rawTx.height = UInt64(receivedTxHeight)
            rawTx.data = receivedTxData
        }
        
        /*
        5. stage 5 empty blocks w/heights received_Tx_height to received_Tx_height + 4
        */
        try coordinator.stageBlockCreate(height: receivedTxHeight, count: 5)
        
        /*
        6. stage tx at received_Tx_height + 3
        */
        let reorgedTxheight = receivedTxHeight + 2
        try coordinator.stageTransaction(receivedRawTx, at: reorgedTxheight)
        
        /*
        6a. applyheight(received_Tx_height + 1)
        */
        try coordinator.applyStaged(blockheight: receivedTxHeight + 1)
        
        sleep(2)

        /*
        7. sync to received_Tx_height + 1
        */
        let reorgSyncexpectation = XCTestExpectation(description: "reorg expectation")
        
        var afterReorgTxTotalBalance = Zatoshi(-1)
        var afterReorgTxVerifiedBalance = Zatoshi(-1)
        
        try coordinator.sync(completion: { synchronizer in
            afterReorgTxTotalBalance = synchronizer.initializer.getBalance()
            afterReorgTxVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
            reorgSyncexpectation.fulfill()
        }, error: self.handleError(_:))
        
        /*
        8. assert that reorg happened at received_Tx_height
        */
        sleep(2)
        wait(for: [reorgExpectation, reorgSyncexpectation], timeout: 5, enforceOrder: false)

        /*
        9. verify that balance equals initial balance
        */
        XCTAssertEqual(afterReorgTxVerifiedBalance, initialVerifiedBalance)
        XCTAssertEqual(afterReorgTxTotalBalance, initialTotalBalance)
        
        /*
        10. sync up to received_Tx_height + 3
        */
        let finalsyncExpectation = XCTestExpectation(description: "final sync")
        
        var finalReorgTxTotalBalance = Zatoshi(-1)
        var finalReorgTxVerifiedBalance = Zatoshi(-1)
        
        try coordinator.applyStaged(blockheight: reorgedTxheight + 1)
        sleep(3)
        try coordinator.sync(completion: { synchronizer in
            finalReorgTxTotalBalance = synchronizer.initializer.getBalance()
            finalReorgTxVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
            finalsyncExpectation.fulfill()
        }, error: self.handleError(_:))
        
        wait(for: [finalsyncExpectation], timeout: 5)
        sleep(3)
        
        guard let reorgedTx = coordinator.synchronizer.receivedTransactions.first else {
            XCTFail("no transactions found")
            return
        }
        
        XCTAssertEqual(reorgedTx.minedHeight, reorgedTxheight)
        XCTAssertEqual(initialVerifiedBalance, finalReorgTxVerifiedBalance)
        XCTAssertEqual(initialTotalBalance + receivedTx.value, finalReorgTxTotalBalance)
    }
    

    /// An outbound, unconfirmed transaction in a specific block changes height in the event of a reorg
    ///
    ///
    /// The wallet handles this change, reflects it appropriately in local storage, and funds remain spendable post confirmation.
    ///
    /// Pre-conditions:
    /// - Wallet has spendable funds
    ///
    /// 1. Setup w/ default dataset
    /// 2. applyStaged(received_Tx_height)
    /// 3. sync up to received_Tx_height
    /// 4. create transaction
    /// 5. stage 10 empty blocks
    /// 6. submit tx at sentTxHeight
    ///   a. getIncomingTx
    ///   b. stageTransaction(sentTx, sentTxHeight)
    ///   c. applyheight(sentTxHeight + 1 )
    /// 7. sync to  sentTxHeight + 2
    /// 8. stage sentTx and otherTx at sentTxheight
    /// 9. applyStaged(sentTx + 2)
    /// 10. sync up to received_Tx_height + 2
    /// 11. verify that the sent tx is mined and balance is correct
    /// 12. applyStaged(sentTx + 10)
    /// 13. verify that there's no more pending transaction
    func testReorgChangesOutboundTxIndex() async throws {
        try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName)
        let receivedTxHeight: BlockHeight = 663188
        var initialTotalBalance = Zatoshi(-1)
        
        /*
        2. applyStaged(received_Tx_height)
        */
        try coordinator.applyStaged(blockheight: receivedTxHeight)
        
        sleep(2)
        let preTxExpectation = XCTestExpectation(description: "pre receive")

        /*
        3. sync up to received_Tx_height
        */
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    initialTotalBalance = synchronizer.initializer.getBalance()
                    continuation.resume()
                    preTxExpectation.fulfill()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }
        
        wait(for: [preTxExpectation], timeout: 5)
        
        let sendExpectation = XCTestExpectation(description: "sendToAddress")
        var pendingEntity: PendingTransactionEntity?
        var testError: Error?
        let sendAmount = Zatoshi(10000)

        /*
        4. create transaction
        */
        do {
            let pendingTx = try await coordinator.synchronizer.sendToAddress(
                spendingKey: coordinator.spendingKeys!.first!,
                zatoshi: sendAmount,
                toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
                memo: try Memo(string: "test transaction")
            )
            pendingEntity = pendingTx
            sendExpectation.fulfill()
        } catch {
            testError = error
            XCTFail("error sending to address. Error: \(String(describing: error))")
        }
        
        wait(for: [sendExpectation], timeout: 2)

        guard let pendingTx = pendingEntity else {
            XCTFail("error sending to address. Error: \(String(describing: testError))")
            return
        }
        
        /*
        5. stage 10 empty blocks
        */
        try coordinator.stageBlockCreate(height: receivedTxHeight + 1, count: 10)
        
        let sentTxHeight = receivedTxHeight + 1
        
        /*
        6. stage sent tx at sentTxHeight
        */
        guard let sentTx = try coordinator.getIncomingTransactions()?.first else {
            XCTFail("sent transaction not present on Darksidewalletd")
            return
        }
        try coordinator.stageTransaction(sentTx, at: sentTxHeight)
        
        /*
        6a. applyheight(sentTxHeight + 1 )
        */
        try coordinator.applyStaged(blockheight: sentTxHeight + 1)
        
        sleep(2)
        
        /*
        7. sync to  sentTxHeight + 1
        */
        let sentTxSyncExpectation = XCTestExpectation(description: "sent tx sync expectation")
        
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight
                    XCTAssertEqual(pMinedHeight, sentTxHeight)
                    continuation.resume()
                    sentTxSyncExpectation.fulfill()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }
        
        wait(for: [sentTxSyncExpectation], timeout: 5)
        
        /*
        8. stage sentTx and otherTx at sentTxheight
        */
        try coordinator.stageBlockCreate(height: sentTxHeight, count: 20, nonce: 5)
        try coordinator.stageTransaction(url: FakeChainBuilder.someOtherTxUrl, at: sentTxHeight)
        try coordinator.stageTransaction(sentTx, at: sentTxHeight)
        
        /*
        9. applyStaged(sentTx + 1)
        */
        try coordinator.applyStaged(blockheight: sentTxHeight + 1)
        
        sleep(2)
        let afterReOrgExpectation = XCTestExpectation(description: "after ReOrg Expectation")
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    /*
                    11. verify that the sent tx is mined and balance is correct
                    */
                    let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight
                    XCTAssertEqual(pMinedHeight, sentTxHeight)
                    XCTAssertEqual(initialTotalBalance - sendAmount - Zatoshi(1000), synchronizer.initializer.getBalance()) // fee change on this branch
                    continuation.resume()
                    afterReOrgExpectation.fulfill()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }
        
        wait(for: [afterReOrgExpectation], timeout: 5)
        
        /*
        12. applyStaged(sentTx + 10)
        */
        try coordinator.applyStaged(blockheight: sentTxHeight + 12)
        sleep(2)

        /*
        13. verify that there's no more pending transaction
        */
        let lastSyncExpectation = XCTestExpectation(description: "sync to confirmation")
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    lastSyncExpectation.fulfill()
                    continuation.resume()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }
        
        wait(for: [lastSyncExpectation], timeout: 5)
        
        XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 0)
        XCTAssertEqual(initialTotalBalance - pendingTx.value - Zatoshi(1000), coordinator.synchronizer.initializer.getVerifiedBalance())

        let resultingBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
        XCTAssertEqual(resultingBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
    }
    
    func testIncomingTransactionIndexChange() throws {
        hookToReOrgNotification()
        self.expectedReorgHeight = 663196
        self.expectedRewindHeight = 663175
        try coordinator.reset(saplingActivation: birthday, branchID: "2bb40e60", chainName: "main")
        try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeBefore))
        try coordinator.applyStaged(blockheight: 663195)
        sleep(1)
        let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
        
        var preReorgTotalBalance = Zatoshi.zero
        var preReorgVerifiedBalance = Zatoshi.zero
        try coordinator.sync(completion: { synchronizer in
            preReorgTotalBalance = synchronizer.initializer.getBalance()
            preReorgVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
            firstSyncExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [firstSyncExpectation], timeout: 10)
        
        /*
        trigger reorg
        */
        try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeAfter))
        try coordinator.applyStaged(blockheight: 663200)
        
        sleep(1)
        
        let afterReorgSync = XCTestExpectation(description: "after reorg sync")
        
        var postReorgTotalBalance = Zatoshi.zero
        var postReorgVerifiedBalance = Zatoshi.zero
        try coordinator.sync(completion: { synchronizer in
            postReorgTotalBalance = synchronizer.initializer.getBalance()
            postReorgVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
            afterReorgSync.fulfill()
        }, error: self.handleError)
        
        wait(for: [reorgExpectation, afterReorgSync], timeout: 30)
        
        XCTAssertEqual(postReorgVerifiedBalance, preReorgVerifiedBalance)
        XCTAssertEqual(postReorgTotalBalance, preReorgTotalBalance)
    }
    
    func testReOrgExpiresInboundTransaction() throws {
        try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
        let receivedTxHeight = BlockHeight(663188)
        try coordinator.applyStaged(blockheight: receivedTxHeight - 1)
        sleep(2)
        let expectation = XCTestExpectation(description: "sync to \(receivedTxHeight - 1) expectation")
        var initialBalance = Zatoshi(-1)
        var initialVerifiedBalance = Zatoshi(-1)
        try coordinator.sync(completion: { synchronizer in
            initialBalance = synchronizer.initializer.getBalance()
            initialVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
            expectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [expectation], timeout: 5)
        
        let afterTxHeight = receivedTxHeight + 1
        try coordinator.applyStaged(blockheight: afterTxHeight)
        sleep(2)
        let afterTxSyncExpectation = XCTestExpectation(description: "sync to \(afterTxHeight) expectation")
        
        var afterTxBalance = Zatoshi(-1)
        var afterTxVerifiedBalance = Zatoshi(-1)
        try coordinator.sync(completion: { synchronizer in
            afterTxBalance = synchronizer.initializer.getBalance()
            afterTxVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
            XCTAssertNotNil(
                synchronizer.receivedTransactions.first { $0.minedHeight == receivedTxHeight },
                "Transaction not found at \(receivedTxHeight)"
            )
            afterTxSyncExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [afterTxSyncExpectation], timeout: 10.0)
        
        XCTAssertEqual(initialVerifiedBalance, afterTxVerifiedBalance)
        XCTAssertNotEqual(initialBalance, afterTxBalance)
        
        let reorgSize: Int = 3
        let newBlocksCount: Int = 11 + reorgSize
        try coordinator.stageBlockCreate(height: receivedTxHeight - reorgSize, count: newBlocksCount + reorgSize)
        try coordinator.applyStaged(blockheight: receivedTxHeight + newBlocksCount - 1)
        
        sleep(2)
        let afterReorgExpectation = XCTestExpectation(description: "after reorg expectation")
        
        var afterReOrgBalance = Zatoshi(-1)
        var afterReOrgVerifiedBalance = Zatoshi(-1)
        try coordinator.sync(completion: { synchronizer in
            afterReOrgBalance = synchronizer.initializer.getBalance()
            afterReOrgVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
            XCTAssertNil(
                synchronizer.receivedTransactions.first { $0.minedHeight == receivedTxHeight },
                "Transaction found at \(receivedTxHeight) after reorg"
            )
            afterReorgExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [afterReorgExpectation], timeout: 5)
        
        XCTAssertEqual(afterReOrgBalance, initialBalance)
        XCTAssertEqual(afterReOrgVerifiedBalance, initialVerifiedBalance)
    }
    

    /// Steps:
    /// 1.  sync up to an incoming transaction (incomingTxHeight + 1)
    /// 1a. save balances
    /// 2. stage 4 blocks from incomingTxHeight - 1 with different nonce
    /// 3. stage otherTx at incomingTxHeight
    /// 4. stage incomingTx at incomingTxHeight
    /// 5. applyHeight(incomingHeight + 3)
    /// 6. sync to latest height
    /// 7. check that balances still match
    func testReOrgChangesInboundTxIndexInBlock() throws {
        try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
        
        let incomingTxHeight = BlockHeight(663188)
        
        try coordinator.applyStaged(blockheight: incomingTxHeight + 1)
        
        /*
        1.  sync up to an incoming transaction (incomingTxHeight + 1)
        */
        let firstSyncExpectation = XCTestExpectation(description: "first sync test expectation")
        
        var initialBalance = Zatoshi(-1)
        var initialVerifiedBalance = Zatoshi(-1)
        var incomingTx: ConfirmedTransactionEntity?
        try coordinator.sync(completion: { _ in
            firstSyncExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [firstSyncExpectation], timeout: 5)
        
        /*
        1a. save balances
        */
        initialBalance = coordinator.synchronizer.initializer.getBalance()
        initialVerifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
        incomingTx = coordinator.synchronizer.receivedTransactions.first(where: { $0.minedHeight == incomingTxHeight })

        guard let transaction = incomingTx else {
            XCTFail("no tx found")
            return
        }
        
        guard let txRawData = transaction.raw else {
            XCTFail("transaction has no raw data")
            return
        }
        
        let rawTransaction = RawTransaction.with({ rawTx in
            rawTx.data = txRawData
        })
        
        /*
        2. stage 4 blocks from incomingTxHeight - 1 with different nonce
        */
        let blockCount = 4
        try coordinator.stageBlockCreate(height: incomingTxHeight - 1, count: blockCount, nonce: Int.random(in: 0 ... Int.max))
        
        /*
        3. stage otherTx at incomingTxHeight
        */
        try coordinator.stageTransaction(url: FakeChainBuilder.someOtherTxUrl, at: incomingTxHeight)
        
        /*
        4. stage incomingTx at incomingTxHeight
        5. applyHeight(incomingHeight + 3)
        6. sync to latest height
        7. check that balances still match
        */
        try coordinator.stageTransaction(rawTransaction, at: incomingTxHeight)
        
        /*
        5. applyHeight(incomingHeight + 2)
        */
        try coordinator.applyStaged(blockheight: incomingTxHeight + 2)
        
        let lastSyncExpectation = XCTestExpectation(description: "last sync expectation")

        /*
        6. sync to latest height
        */
        try coordinator.sync(completion: { _ in
            lastSyncExpectation.fulfill()
        }, error: self.handleError)
        
        /*
        7. check that balances still match
        */
        XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), initialVerifiedBalance)
        XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), initialBalance)
        
        wait(for: [lastSyncExpectation], timeout: 5)
    }
    
    func testTxIndexReorg() throws {
        try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeBefore))
        
        let txReorgHeight = BlockHeight(663195)
        let finalHeight = BlockHeight(663200)
        try coordinator.applyStaged(blockheight: txReorgHeight)
        let firstSyncExpectation = XCTestExpectation(description: "first sync test expectation")
        var initialBalance = Zatoshi(-1)
        var initialVerifiedBalance = Zatoshi(-1)
        try coordinator.sync(completion: { synchronizer in
            initialBalance = synchronizer.initializer.getBalance()
            initialVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
            firstSyncExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [firstSyncExpectation], timeout: 5)
        
        try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeAfter))
        
        try coordinator.applyStaged(blockheight: finalHeight)
        
        let lastSyncExpectation = XCTestExpectation(description: "last sync expectation")
        
        try coordinator.sync(completion: { _ in
            lastSyncExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [lastSyncExpectation], timeout: 5)
        
        XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), initialBalance)
        XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), initialVerifiedBalance)
    }

    /// A Re Org occurs and changes the height of an outbound transaction
    /// Pre-condition: Wallet has funds
    ///
    /// Steps:
    /// 1. create fake chain
    /// 1a. sync to latest height
    /// 2. send transaction to recipient address
    /// 3. getIncomingTransaction
    /// 4. stage transaction at sentTxHeight
    /// 5. applyHeight(sentTxHeight)
    /// 6. sync to latest height
    /// 6a. verify that there's a pending transaction with a mined height of sentTxHeight
    /// 7. stage 15  blocks from sentTxHeight
    /// 7. a stage sent tx to sentTxHeight + 2
    /// 8. applyHeight(sentTxHeight + 1) to cause a 1 block reorg
    /// 9. sync to latest height
    /// 10. verify that there's a pending transaction with -1 mined height
    /// 11. applyHeight(sentTxHeight + 2)
    /// 11a. sync to latest height
    /// 12. verify that there's a pending transaction with a mined height of sentTxHeight + 2
    /// 13. apply height(sentTxHeight + 15)
    /// 14. sync to latest height
    /// 15. verify that there's no pending transaction and that the tx is displayed on the sentTransactions collection
    func testReOrgChangesOutboundTxMinedHeight() async throws {
        hookToReOrgNotification()

        /*
        1. create fake chain
        */
        try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
        
        try coordinator.applyStaged(blockheight: 663188)
        sleep(2)
        
        let firstSyncExpectation = XCTestExpectation(description: "first sync")
        /*
        1a. sync to latest height
        */
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    firstSyncExpectation.fulfill()
                    continuation.resume()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }
        
        wait(for: [firstSyncExpectation], timeout: 5)
        
        sleep(1)
        let initialTotalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
        
        let sendExpectation = XCTestExpectation(description: "send expectation")
        var pendingEntity: PendingTransactionEntity?
        
        /*
        2. send transaction to recipient address
        */
        do {
            let pendingTx = try await coordinator.synchronizer.sendToAddress(
                spendingKey: self.coordinator.spendingKeys!.first!,
                zatoshi: Zatoshi(20000),
                toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
                memo: try Memo(string: "this is a test")
            )
            pendingEntity = pendingTx
            sendExpectation.fulfill()
        } catch {
            self.handleError(error)
        }
        
        wait(for: [sendExpectation], timeout: 11)
        
        guard pendingEntity != nil else {
            XCTFail("no pending transaction after sending")
            try coordinator.stop()
            return
        }

        /**
        3. getIncomingTransaction
        */
        guard let incomingTx = try coordinator.getIncomingTransactions()?.first else {
            XCTFail("no incoming transaction")
            try coordinator.stop()
            return
        }
        
        let sentTxHeight: BlockHeight = 663189
        
        /*
        4. stage transaction at sentTxHeight
        */
        try coordinator.stageBlockCreate(height: sentTxHeight)
        
        try coordinator.stageTransaction(incomingTx, at: sentTxHeight)

        /*
        5. applyHeight(sentTxHeight)
        */
        try coordinator.applyStaged(blockheight: sentTxHeight)
        
        sleep(2)
        
        /*
        6. sync to latest height
        */
        let secondSyncExpectation = XCTestExpectation(description: "after send expectation")
        
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    secondSyncExpectation.fulfill()
                    continuation.resume()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }

        wait(for: [secondSyncExpectation], timeout: 5)
        
        XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 1)
        guard let afterStagePendingTx = coordinator.synchronizer.pendingTransactions.first else {
            return
        }
        
        /*
        6a. verify that there's a pending transaction with a mined height of sentTxHeight
        */
        XCTAssertEqual(afterStagePendingTx.minedHeight, sentTxHeight)
        
        /*
        7. stage 20  blocks from sentTxHeight
        */
        try coordinator.stageBlockCreate(height: sentTxHeight, count: 25)
        
        /*
        7a. stage sent tx to sentTxHeight + 2
        */
        try coordinator.stageTransaction(incomingTx, at: sentTxHeight + 2)
        
        /*
        8. applyHeight(sentTxHeight + 1) to cause a 1 block reorg
        */
        try coordinator.applyStaged(blockheight: sentTxHeight + 1)
        sleep(2)
        
        /*
        9. sync to latest height
        */
        self.expectedReorgHeight = sentTxHeight + 1
        let afterReorgExpectation = XCTestExpectation(description: "after reorg sync")
        
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    afterReorgExpectation.fulfill()
                    continuation.resume()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }

        wait(for: [reorgExpectation, afterReorgExpectation], timeout: 5)
        
        /*
        10. verify that there's a pending transaction with -1 mined height
        */
        guard let newPendingTx = coordinator.synchronizer.pendingTransactions.first else {
            XCTFail("No pending transaction")
            try coordinator.stop()
            return
        }
        
        XCTAssertEqual(newPendingTx.minedHeight, BlockHeight.empty())
        
        /*
        11. applyHeight(sentTxHeight + 2)
        */
        try coordinator.applyStaged(blockheight: sentTxHeight + 2)
        sleep(2)
        
        let yetAnotherExpectation = XCTestExpectation(description: "after staging expectation")
        
        /*
        11a. sync to latest height
        */
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    yetAnotherExpectation.fulfill()
                    continuation.resume()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }

        wait(for: [yetAnotherExpectation], timeout: 5)
        
        /*
        12. verify that there's a pending transaction with a mined height of sentTxHeight + 2
        */
        XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 1)
        guard let newlyPendingTx = try coordinator.synchronizer.allPendingTransactions().first else {
            XCTFail("no pending transaction")
            try coordinator.stop()
            return
        }
        
        XCTAssertEqual(newlyPendingTx.minedHeight, sentTxHeight + 2)
        
        /*
        13. apply height(sentTxHeight + 25)
        */
        try coordinator.applyStaged(blockheight: sentTxHeight + 25)
        
        sleep(2)
        
        let thisIsTheLastExpectationIPromess = XCTestExpectation(description: "last sync")

        /*
        14. sync to latest height
        */
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    thisIsTheLastExpectationIPromess.fulfill()
                    continuation.resume()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }

        wait(for: [thisIsTheLastExpectationIPromess], timeout: 5)
        
        /*
        15. verify that there's no pending transaction and that the tx is displayed on the sentTransactions collection
        */
        XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 0)
        XCTAssertNotNil(
            coordinator.synchronizer.sentTransactions
                .first(
                    where: { transaction in
                        guard let txId = transaction.rawTransactionId else { return false }
                        return txId == newlyPendingTx.rawTransactionId
                    }
                ),
            "Sent Tx is not on sent transactions"
        )
        
        XCTAssertEqual(
            initialTotalBalance - newlyPendingTx.value - Zatoshi(1000),
            coordinator.synchronizer.initializer.getBalance()
        )

        XCTAssertEqual(
            initialTotalBalance - newlyPendingTx.value - Zatoshi(1000),
            coordinator.synchronizer.initializer.getVerifiedBalance()
        )
    }


    /// Uses the zcash-hackworks data set.

    /// A Re Org occurs at 663195, and sweeps an Inbound Tx that appears later on the chain.
    /// Steps:
    /// 1. reset dlwd
    /// 2. load blocks from txHeightReOrgBefore
    /// 3. applyStaged(663195)
    /// 4. sync to latest height
    /// 5. get balances
    /// 6. load blocks from dataset txHeightReOrgBefore
    /// 7. apply stage 663200
    /// 8. sync to latest height
    /// 9. verify that the balance is equal to the one before the reorg
    func testReOrgChangesInboundMinedHeight() throws {
        try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName)
        sleep(2)
        try coordinator.resetBlocks(dataset: .predefined(dataset: .txHeightReOrgBefore))
        sleep(2)
        try coordinator.applyStaged(blockheight: 663195)
        sleep(2)
        let firstSyncExpectation = XCTestExpectation(description: "first sync")
        
        try coordinator.sync(completion: { _ in
            firstSyncExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [firstSyncExpectation], timeout: 5)
        
        let initialBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
        let initialVerifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
        guard let initialTxHeight = try coordinator.synchronizer.allReceivedTransactions().first?.minedHeight else {
            XCTFail("no incoming transaction found!")
            return
        }
        
        try coordinator.resetBlocks(dataset: .predefined(dataset: .txHeightReOrgAfter))
        
        sleep(5)
        
        try coordinator.applyStaged(blockheight: 663200)
        
        sleep(6)
        
        let afterReOrgExpectation = XCTestExpectation(description: "after reorg")
        try coordinator.sync(completion: { _ in
            afterReOrgExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [afterReOrgExpectation], timeout: 5)
        
        guard let afterReOrgTxHeight = coordinator.synchronizer.receivedTransactions.first?.minedHeight else {
            XCTFail("no incoming transaction found after re org!")
            return
        }
        XCTAssertEqual(initialVerifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
        XCTAssertEqual(initialBalance, coordinator.synchronizer.initializer.getBalance())
        XCTAssert(afterReOrgTxHeight > initialTxHeight)
    }

    /// Re Org removes incoming transaction and is never mined
    /// Steps:
    /// 1. sync prior to incomingTxHeight - 1 to get balances there
    /// 2. sync to latest height
    /// 3. cause reorg
    /// 4. sync to latest height
    /// 5. verify that reorg Happened at reorgHeight
    /// 6. verify that balances match initial balances
    func testReOrgRemovesIncomingTxForever() throws {
        hookToReOrgNotification()
        try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName)
        
        try coordinator.resetBlocks(dataset: .predefined(dataset: .txReOrgRemovesInboundTxBefore))
        
        let reorgHeight: BlockHeight = 663195
        self.expectedReorgHeight = reorgHeight
        self.expectedRewindHeight = reorgHeight - 10
        
        try coordinator.applyStaged(blockheight: reorgHeight - 1)
        
        sleep(2)
        let firstSyncExpectation = XCTestExpectation(description: "first sync")
        
        /**
        1. sync prior to incomingTxHeight - 1 to get balances there
        */
        try coordinator.sync(completion: { _ in
            firstSyncExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [firstSyncExpectation], timeout: 5)
        
        let initialTotalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
        let initialVerifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
        
        try coordinator.applyStaged(blockheight: reorgHeight)
        
        let secondSyncExpectation = XCTestExpectation(description: "second sync expectation")
        
        /**
        2. sync to latest height
        */
        try coordinator.sync(completion: { _ in
            secondSyncExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [secondSyncExpectation], timeout: 10)
        
        /**
        3. cause reorg
        */
        try coordinator.resetBlocks(dataset: .predefined(dataset: .txReOrgRemovesInboundTxAfter))
        
        try coordinator.applyStaged(blockheight: 663200)
        sleep(2)
        
        let afterReorgSyncExpectation = XCTestExpectation(description: "after reorg expectation")
        try coordinator.sync(completion: { _ in
            afterReorgSyncExpectation.fulfill()
        }, error: self.handleError)
        
        wait(for: [afterReorgSyncExpectation], timeout: 5)
        
        XCTAssertEqual(initialVerifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
        XCTAssertEqual(initialTotalBalance, coordinator.synchronizer.initializer.getBalance())
    }
    
    /// Transaction was included in a block, and then is not included in a block after a reorg, and expires.
    /// Steps:
    /// 1. create fake chain
    /// 1a. sync to latest height
    /// 2. send transaction to recipient address
    /// 3. getIncomingTransaction
    /// 4. stage transaction at sentTxHeight
    /// 5. applyHeight(sentTxHeight)
    /// 6. sync to latest height
    /// 6a. verify that there's a pending transaction with a mined height of sentTxHeight
    /// 7. stage 15 blocks from sentTxHeigth to cause a reorg
    /// 8. sync to latest height
    /// 9. verify that there's an expired transaction as a pending transaction
    func testReOrgRemovesOutboundTxAndIsNeverMined() async throws {
        hookToReOrgNotification()
        
        /*
        1. create fake chain
        */
        try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
        let sentTxHeight: BlockHeight = 663195
        try coordinator.applyStaged(blockheight: sentTxHeight - 1)
        
        sleep(2)
        
        let firstSyncExpectation = XCTestExpectation(description: "first sync")

        /*
        1a. sync to latest height
        */
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    
                    continuation.resume()
                    firstSyncExpectation.fulfill()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }

        wait(for: [firstSyncExpectation], timeout: 10)
        
        sleep(1)
        let initialTotalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
        
        let sendExpectation = XCTestExpectation(description: "send expectation")
        var pendingEntity: PendingTransactionEntity?
        
        /*
        2. send transaction to recipient address
        */
        do {
            let pendingTx = try await coordinator.synchronizer.sendToAddress(
                spendingKey: self.coordinator.spendingKeys!.first!,
                zatoshi: Zatoshi(20000),
                toAddress: try Recipient(testRecipientAddress, network: self.network.networkType),
                memo: try! Memo(string: "this is a test")
        )
            pendingEntity = pendingTx
            sendExpectation.fulfill()
        } catch {
            self.handleError(error)
        }
        
        wait(for: [sendExpectation], timeout: 11)
        
        guard pendingEntity != nil else {
            XCTFail("no pending transaction after sending")
            try coordinator.stop()
            return
        }

        /**
        3. getIncomingTransaction
        */
        guard let incomingTx = try coordinator.getIncomingTransactions()?.first else {
            XCTFail("no incoming transaction")
            try coordinator.stop()
            return
        }
        
        self.expectedReorgHeight = sentTxHeight + 1

        /*
        4. stage transaction at sentTxHeight
        */
        try coordinator.stageBlockCreate(height: sentTxHeight)
        
        try coordinator.stageTransaction(incomingTx, at: sentTxHeight)

        /*
        5. applyHeight(sentTxHeight)
        */
        try coordinator.applyStaged(blockheight: sentTxHeight)
        
        sleep(2)
        
        /*
        6. sync to latest height
        */
        let secondSyncExpectation = XCTestExpectation(description: "after send expectation")
        
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    secondSyncExpectation.fulfill()
                    continuation.resume()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }

        wait(for: [secondSyncExpectation], timeout: 5)
        let extraBlocks = 25
        try coordinator.stageBlockCreate(height: sentTxHeight, count: extraBlocks, nonce: 5)
        
        try coordinator.applyStaged(blockheight: sentTxHeight + 5)
        
        sleep(2)
        let reorgSyncExpectation = XCTestExpectation(description: "reorg sync expectation")
        
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    reorgSyncExpectation.fulfill()
                    continuation.resume()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }

        wait(for: [reorgExpectation, reorgSyncExpectation], timeout: 5)
        
        guard let pendingTx = coordinator.synchronizer.pendingTransactions.first else {
            XCTFail("no pending transaction after reorg sync")
            return
        }
        
        XCTAssertFalse(pendingTx.isMined)
        
        LoggerProxy.info("applyStaged(blockheight: \(sentTxHeight + extraBlocks - 1))")
        try coordinator.applyStaged(blockheight: sentTxHeight + extraBlocks - 1)
        
        sleep(2)
        
        let lastSyncExpectation = XCTestExpectation(description: "last sync expectation")
        
        try await withCheckedThrowingContinuation { continuation in
            do {
                try coordinator.sync(completion: { synchronizer in
                    lastSyncExpectation.fulfill()
                    continuation.resume()
                }, error: self.handleError)
            } catch {
                continuation.resume(throwing: error)
            }
        }

        wait(for: [lastSyncExpectation], timeout: 5)
        
        XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), initialTotalBalance)
    }
    
    func testLongSync() throws {
        hookToReOrgNotification()
        
        /*
        1. create fake chain
        */
        let fullSyncLength = 100_000
    
        try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: fullSyncLength)
        
        try coordinator.applyStaged(blockheight: birthday + fullSyncLength)
        
        sleep(10)
        
        let firstSyncExpectation = XCTestExpectation(description: "first sync")

        /*
        sync to latest height
        */
        try coordinator.sync(completion: { _ in
            firstSyncExpectation.fulfill()
        }, error: { error in
            _ = try? self.coordinator.stop()
            firstSyncExpectation.fulfill()
            guard let testError = error else {
                XCTFail("failed with nil error")
                return
            }
            XCTFail("Failed with error: \(testError)")
        })
        
        wait(for: [firstSyncExpectation], timeout: 500)
        
        XCTAssertEqual(try coordinator.synchronizer.latestDownloadedHeight(), birthday + fullSyncLength)
    }
    
    func handleError(_ error: Error?) {
        _ = try? coordinator.stop()
        guard let testError = error else {
            XCTFail("failed with nil error")
            return
        }
        XCTFail("Failed with error: \(testError)")
    }
    
    func hookToReOrgNotification() {
        NotificationCenter.default.addObserver(self, selector: #selector(handleReorg(_:)), name: .blockProcessorHandledReOrg, object: nil)
    }
}
