//
//  BlockBatchValidationTests.swift
//  ZcashLightClientKit-Unit-Tests
//
//  Created by Francisco Gindre on 6/17/21.
//

import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit

// swiftlint:disable force_try type_body_length
class BlockBatchValidationTests: XCTestCase {
    func testBranchIdFailure() async throws {
        let network = ZcashNetworkBuilder.network(for: .mainnet)
        let service = MockLightWalletService(
            latestBlockHeight: 1210000,
            service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
        )
        let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
        let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
        let downloader = CompactBlockDownloader(service: service, storage: repository)
        let config = CompactBlockProcessor.Configuration(
            cacheDb: try!  __cacheDbURL(),
            dataDb: try! __dataDbURL(),
            downloadBatchSize: 100,
            retries: 5,
            maxBackoffInterval: 10,
            rewindDistance: 100,
            walletBirthday: 1210000,
            saplingActivation: network.constants.saplingActivationHeight,
            network: network
        )

        var info = LightdInfo()
        info.blockHeight = 130000
        info.branch = "d34db33f"
        info.chainName = "main"
        info.buildUser = "test user"
        info.consensusBranchID = "d34db33f"
        info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)
        service.mockLightDInfo = info
        
        let mockRust = MockRustBackend.self
        mockRust.consensusBranchID = Int32(0xd34d)
        
        let compactBlockProcessor = CompactBlockProcessor(
            service: service,
            storage: storage,
            backend: mockRust,
            config: config
        )
        
        do {
            try await compactBlockProcessor.figureNextBatch(downloader: downloader)
            XCTAssertFalse(Task.isCancelled)
        } catch {
            switch error {
            case CompactBlockProcessorError.wrongConsensusBranchId:
                break
            default:
                XCTFail("Expected CompactBlockProcessorError.wrongConsensusBranchId but found \(error)")
            }
        }
    }
    
    func testBranchNetworkMismatchFailure() async throws {
        let network = ZcashNetworkBuilder.network(for: .mainnet)
        let service = MockLightWalletService(
            latestBlockHeight: 1210000,
            service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
        )
        let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
        let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
        let downloader = CompactBlockDownloader(service: service, storage: repository)
        let config = CompactBlockProcessor.Configuration(
            cacheDb: try!  __cacheDbURL(),
            dataDb: try! __dataDbURL(),
            downloadBatchSize: 100,
            retries: 5,
            maxBackoffInterval: 10,
            rewindDistance: 100,
            walletBirthday: 1210000,
            saplingActivation: network.constants.saplingActivationHeight,
            network: network
        )
        var info = LightdInfo()
        info.blockHeight = 130000
        info.branch = "d34db33f"
        info.chainName = "test"
        info.buildUser = "test user"
        info.consensusBranchID = "d34db4d"
        info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)

        service.mockLightDInfo = info
        
        let mockRust = MockRustBackend.self
        mockRust.consensusBranchID = 0xd34db4d
        
        let compactBlockProcessor = CompactBlockProcessor(
            service: service,
            storage: storage,
            backend: mockRust,
            config: config
        )
        
        do {
            try await compactBlockProcessor.figureNextBatch(downloader: downloader)
            XCTAssertFalse(Task.isCancelled)
        } catch {
            switch error {
            case CompactBlockProcessorError.networkMismatch(expected: .mainnet, found: .testnet):
                break
            default:
                XCTFail("Expected CompactBlockProcessorError.networkMismatch but found \(error)")
            }
        }
    }
    
    func testBranchNetworkTypeWrongFailure() async throws {
        let network = ZcashNetworkBuilder.network(for: .testnet)
        let service = MockLightWalletService(
            latestBlockHeight: 1210000,
            service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
        )
        let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
        let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
        let downloader = CompactBlockDownloader(service: service, storage: repository)
        let config = CompactBlockProcessor.Configuration(
            cacheDb: try!  __cacheDbURL(),
            dataDb: try! __dataDbURL(),
            downloadBatchSize: 100,
            retries: 5,
            maxBackoffInterval: 10,
            rewindDistance: 100,
            walletBirthday: 1210000,
            saplingActivation: network.constants.saplingActivationHeight,
            network: network
        )
        var info = LightdInfo()
        info.blockHeight = 130000
        info.branch = "d34db33f"
        info.chainName = "another"
        info.buildUser = "test user"
        info.consensusBranchID = "d34db4d"
        info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)

        service.mockLightDInfo = info
        
        let mockRust = MockRustBackend.self
        mockRust.consensusBranchID = 0xd34db4d
        
        let compactBlockProcessor = CompactBlockProcessor(
            service: service,
            storage: storage,
            backend: mockRust,
            config: config
        )
        
        do {
            try await compactBlockProcessor.figureNextBatch(downloader: downloader)
            XCTAssertFalse(Task.isCancelled)
        } catch {
            switch error {
            case CompactBlockProcessorError.generalError:
                break
            default:
                XCTFail("Expected CompactBlockProcessorError.generalError but found \(error)")
            }
        }
    }
    
    func testSaplingActivationHeightMismatch() async throws {
        let network = ZcashNetworkBuilder.network(for: .mainnet)
        let service = MockLightWalletService(
            latestBlockHeight: 1210000,
            service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
        )
        let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
        let repository = ZcashConsoleFakeStorage(latestBlockHeight: 1220000)
        let downloader = CompactBlockDownloader(service: service, storage: repository)
        let config = CompactBlockProcessor.Configuration(
            cacheDb: try!  __cacheDbURL(),
            dataDb: try! __dataDbURL(),
            downloadBatchSize: 100,
            retries: 5,
            maxBackoffInterval: 10,
            rewindDistance: 100,
            walletBirthday: 1210000,
            saplingActivation: network.constants.saplingActivationHeight,
            network: network
        )

        var info = LightdInfo()
        info.blockHeight = 130000
        info.branch = "d34db33f"
        info.chainName = "main"
        info.buildUser = "test user"
        info.consensusBranchID = "d34db4d"
        info.saplingActivationHeight = UInt64(3434343)

        service.mockLightDInfo = info
        
        let mockRust = MockRustBackend.self
        mockRust.consensusBranchID = 0xd34db4d
        
        let compactBlockProcessor = CompactBlockProcessor(
            service: service,
            storage: storage,
            backend: mockRust,
            config: config
        )
        
        do {
            try await compactBlockProcessor.figureNextBatch(downloader: downloader)
            XCTAssertFalse(Task.isCancelled)
        } catch {
            switch error {
            case CompactBlockProcessorError.saplingActivationMismatch(
                expected: network.constants.saplingActivationHeight,
                found: BlockHeight(info.saplingActivationHeight)
            ):
                break
            default:
                XCTFail("Expected CompactBlockProcessorError.saplingActivationMismatch but found \(error)")
            }
        }
    }
    
    func testResultIsWait() async throws {
        let network = ZcashNetworkBuilder.network(for: .mainnet)
        
        let expectedLatestHeight = BlockHeight(1210000)
        let service = MockLightWalletService(
            latestBlockHeight: expectedLatestHeight,
            service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
        )
        let expectedStoreLatestHeight = BlockHeight(1220000)
        let expectedResult = CompactBlockProcessor.NextState.wait(
            latestHeight: expectedLatestHeight,
            latestDownloadHeight: expectedLatestHeight
        )
        let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
        let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoreLatestHeight)
        let downloader = CompactBlockDownloader(service: service, storage: repository)
        let config = CompactBlockProcessor.Configuration(
            cacheDb: try!  __cacheDbURL(),
            dataDb: try! __dataDbURL(),
            downloadBatchSize: 100,
            retries: 5,
            maxBackoffInterval: 10,
            rewindDistance: 100,
            walletBirthday: 1210000,
            saplingActivation: network.constants.saplingActivationHeight,
            network: network
        )

        var info = LightdInfo()
        info.blockHeight = UInt64(expectedLatestHeight)
        info.branch = "d34db33f"
        info.chainName = "main"
        info.buildUser = "test user"
        info.consensusBranchID = "d34db4d"
        info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)

        service.mockLightDInfo = info
        
        let mockRust = MockRustBackend.self
        mockRust.consensusBranchID = 0xd34db4d
        
        let compactBlockProcessor = CompactBlockProcessor(
            service: service,
            storage: storage,
            backend: mockRust,
            config: config
        )
        
        var nextBatch: CompactBlockProcessor.NextState?
        do {
            nextBatch = try await compactBlockProcessor.figureNextBatch(downloader: downloader)
            XCTAssertFalse(Task.isCancelled)
        } catch {
            XCTFail("this shouldn't happen: \(error)")
        }
        
        guard let _ = nextBatch else {
            XCTFail("result should not be nil")
            return
        }
        
        XCTAssertTrue(
            {
                switch nextBatch {
                case .wait(latestHeight: expectedLatestHeight, latestDownloadHeight: expectedLatestHeight):
                    return true
                default:
                    return false
                }
            }(),
            "Expected \(expectedResult) got: \(String(describing: nextBatch))"
        )
    }
    
    func testResultProcessNew() async throws {
        let network = ZcashNetworkBuilder.network(for: .mainnet)
        let expectedLatestHeight = BlockHeight(1230000)
        let service = MockLightWalletService(
            latestBlockHeight: expectedLatestHeight,
            service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
        )
        let expectedStoreLatestHeight = BlockHeight(1220000)
        let walletBirthday = BlockHeight(1210000)
        let expectedResult = CompactBlockProcessor.NextState.processNewBlocks(
            range: CompactBlockProcessor.nextBatchBlockRange(
                latestHeight: expectedLatestHeight,
                latestDownloadedHeight: expectedStoreLatestHeight,
                walletBirthday: walletBirthday
            )
        )
        let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
        let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoreLatestHeight)
        let downloader = CompactBlockDownloader(service: service, storage: repository)
        let config = CompactBlockProcessor.Configuration(
            cacheDb: try!  __cacheDbURL(),
            dataDb: try! __dataDbURL(),
            downloadBatchSize: 100,
            retries: 5,
            maxBackoffInterval: 10,
            rewindDistance: 100,
            walletBirthday: walletBirthday,
            saplingActivation: network.constants.saplingActivationHeight,
            network: network
        )

        var info = LightdInfo()
        info.blockHeight = UInt64(expectedLatestHeight)
        info.branch = "d34db33f"
        info.chainName = "main"
        info.buildUser = "test user"
        info.consensusBranchID = "d34db4d"
        info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)

        service.mockLightDInfo = info
        
        let mockRust = MockRustBackend.self
        mockRust.consensusBranchID = 0xd34db4d
        
        let compactBlockProcessor = CompactBlockProcessor(
            service: service,
            storage: storage,
            backend: mockRust,
            config: config
        )
        
        var nextBatch: CompactBlockProcessor.NextState?
        do {
            nextBatch = try await compactBlockProcessor.figureNextBatch(downloader: downloader)
            XCTAssertFalse(Task.isCancelled)
        } catch {
            XCTFail("this shouldn't happen: \(error)")
        }
        
        guard let _ = nextBatch else {
            XCTFail("result should not be nil")
            return
        }
        
        XCTAssertTrue(
            {
                switch nextBatch {
                case .processNewBlocks(range: CompactBlockRange(uncheckedBounds: (expectedStoreLatestHeight + 1, expectedLatestHeight))):
                    return true
                default:
                    return false
                }
            }(),
            "Expected \(expectedResult) got: \(String(describing: nextBatch))"
        )
    }
    
    func testResultProcessorFinished() async throws {
        let network = ZcashNetworkBuilder.network(for: .mainnet)
        let expectedLatestHeight = BlockHeight(1230000)
        let service = MockLightWalletService(
            latestBlockHeight: expectedLatestHeight,
            service: LightWalletGRPCService(endpoint: LightWalletEndpointBuilder.default)
        )
        let expectedStoreLatestHeight = BlockHeight(1230000)
        let walletBirthday = BlockHeight(1210000)
        let expectedResult = CompactBlockProcessor.NextState.finishProcessing(height: expectedStoreLatestHeight)
        let storage = try! TestDbBuilder.inMemoryCompactBlockStorage()
        let repository = ZcashConsoleFakeStorage(latestBlockHeight: expectedStoreLatestHeight)
        let downloader = CompactBlockDownloader(service: service, storage: repository)
        let config = CompactBlockProcessor.Configuration(
            cacheDb: try!  __cacheDbURL(),
            dataDb: try! __dataDbURL(),
            downloadBatchSize: 100,
            retries: 5,
            maxBackoffInterval: 10,
            rewindDistance: 100,
            walletBirthday: walletBirthday,
            saplingActivation: network.constants.saplingActivationHeight,
            network: network
        )

        var info = LightdInfo()
        info.blockHeight = UInt64(expectedLatestHeight)
        info.branch = "d34db33f"
        info.chainName = "main"
        info.buildUser = "test user"
        info.consensusBranchID = "d34db4d"
        info.saplingActivationHeight = UInt64(network.constants.saplingActivationHeight)

        service.mockLightDInfo = info
        
        let mockRust = MockRustBackend.self
        mockRust.consensusBranchID = 0xd34db4d
        
        let compactBlockProcessor = CompactBlockProcessor(
            service: service,
            storage: storage,
            backend: mockRust,
            config: config
        )
        
        var nextBatch: CompactBlockProcessor.NextState?
        do {
            nextBatch = try await compactBlockProcessor.figureNextBatch(downloader: downloader)
            XCTAssertFalse(Task.isCancelled)
        } catch {
            XCTFail("this shouldn't happen: \(error)")
        }
        
        guard let _ = nextBatch else {
            XCTFail("result should not be nil")
            return
        }
        
        XCTAssertTrue(
            {

                switch nextBatch {
                case .finishProcessing(height: expectedLatestHeight):
                    return true
                default:
                    return false
                }
            }(),
            "Expected \(expectedResult) got: \(String(describing: nextBatch))"
        )
    }
}
