//
//  WalletConfigProviderTests.swift
//  secantTests
//
//  Created by Michal Fousek on 23.02.2023.
//

import Combine
import XCTest
import ComposableArchitecture
import WalletConfigProvider
import Models
import Root
import ZcashLightClientKit
@testable import secant_testnet

class WalletConfigProviderTests: XCTestCase {
    var cancellables: [AnyCancellable] = []

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

    override func tearDown() {
        super.tearDown()
        cancellables = []
    }

    func testTestFlagsAreDisabledByDefault() {
        XCTAssertFalse(WalletConfig.initial.isEnabled(.testFlag1))
        XCTAssertFalse(WalletConfig.initial.isEnabled(.testFlag2))
    }

    func testLoadFlagsFromProvider() async {
        let sourceProvider = WalletConfigSourceProviderMock() {
            return WalletConfig(flags: [.testFlag1: true, .testFlag2: false])
        }

        let provider = WalletConfigProvider(configSourceProvider: sourceProvider, cache: UserDefaultsWalletConfigStorage())
        let configuration = await loadWalletConfig(provider)

        XCTAssertTrue(configuration.isEnabled(.testFlag1))
        XCTAssertFalse(configuration.isEnabled(.testFlag2))
    }

    func testLoadFlagsFromCache() async {
        let sourceProvider = WalletConfigSourceProviderMock() { throw NSError(domain: "whatever", code: 21) }
        let cache = WalletConfigProviderCacheMock(cachedFlags: [.testFlag1: false, .testFlag2: true])

        let provider = WalletConfigProvider(configSourceProvider: sourceProvider, cache: cache)
        let configuration = await loadWalletConfig(provider)

        XCTAssertFalse(configuration.isEnabled(.testFlag1))
        XCTAssertTrue(configuration.isEnabled(.testFlag2))
    }

    func testLoadDefaultFlags() async {
        let sourceProvider = WalletConfigSourceProviderMock() { throw NSError(domain: "whatever", code: 21) }
        let cache = WalletConfigProviderCacheMock(cachedFlags: [:])

        let provider = WalletConfigProvider(configSourceProvider: sourceProvider, cache: cache)
        let configuration = await loadWalletConfig(provider)

        XCTAssertFalse(configuration.isEnabled(.testFlag1))
        XCTAssertFalse(configuration.isEnabled(.testFlag2))
    }

    func testAllTheFlagsAreAlwaysReturned() async {
        let sourceProvider = WalletConfigSourceProviderMock() {
            return WalletConfig(flags: [.testFlag1: true])
        }

        let provider = WalletConfigProvider(configSourceProvider: sourceProvider, cache: UserDefaultsWalletConfigStorage())
        let configuration = await loadWalletConfig(provider)

        XCTAssertTrue(configuration.isEnabled(.testFlag1))
        XCTAssertFalse(configuration.isEnabled(.testFlag2))
    }

    func testProvidedFlagsAreCached() async {
        let sourceProvider = WalletConfigSourceProviderMock() {
            return WalletConfig(flags: [.testFlag1: true, .testFlag2: false])
        }
        let cache: WalletConfigProviderCache = UserDefaultsWalletConfigProviderCache()

        let provider = WalletConfigProvider(configSourceProvider: sourceProvider, cache: cache)
        _ = await loadWalletConfig(provider)

        guard let cachedConfiguration = await cache.load() else {
            return XCTFail("No cached configuration.")
        }

        XCTAssertTrue(cachedConfiguration.isEnabled(.testFlag1))
        XCTAssertFalse(cachedConfiguration.isEnabled(.testFlag2))
    }

    private func loadWalletConfig(_ provider: WalletConfigProvider) async -> WalletConfig {
        let expectation = XCTestExpectation()
        expectation.expectedFulfillmentCount = 2
        var configuration: WalletConfig!

        await withCheckedContinuation { continuation in
            provider.load()
                .sink(
                    receiveCompletion: { _ in
                        expectation.fulfill()
                        continuation.resume()
                    },
                    receiveValue: {
                        configuration = $0
                        expectation.fulfill()
                    }
                )
                .store(in: &cancellables)
        }

        await fulfillment(of: [expectation], timeout: 1)

        return configuration
    }
    
    @MainActor func testPropagationOfFlagUpdate() async throws {
        let store = TestStore(
            initialState: .initial
        ) {
            RootReducer()
        }
        
        // Change any of the flags from the default value
        var defaultRawFlags = WalletConfig.initial.flags
        defaultRawFlags[.onboardingFlow] = true
        let flags = WalletConfig(flags: defaultRawFlags)
        
        await store.send(.debug(.walletConfigLoaded(flags)))
        
        // The new flag's value has to be propagated to all `walletConfig` instances
        await store.receive(.updateStateAfterConfigUpdate(flags)) { state in
            state.walletConfig = flags
            state.onboardingState.walletConfig = flags
            state.tabsState.homeState.walletConfig = flags
        }
        
        await store.finish()
    }
}

struct WalletConfigSourceProviderMock: WalletConfigSourceProvider {
    let provider: () async throws -> WalletConfig

    func load() async throws -> WalletConfig {
        return try await provider()
    }
}

class WalletConfigProviderCacheMock: WalletConfigProviderCache {
    var cachedFlags: WalletConfig.RawFlags = [:]

    init(cachedFlags: WalletConfig.RawFlags) {
        self.cachedFlags = cachedFlags
    }

    func load() async -> WalletConfig? {
        guard !cachedFlags.isEmpty else { return nil }
        return WalletConfig(flags: cachedFlags)
    }

    func store(_ configuration: WalletConfig) async {
        cachedFlags = configuration.flags
    }
}

extension UserDefaultsWalletConfigStorage {
    func clearAll() {
        let userdef = UserDefaults.standard
        userdef.removeObject(forKey: Constants.cacheKey)
        userdef.removeObject(forKey: Constants.providerKey)
        userdef.synchronize()
    }
}
