//
//  MockTransactionRepository.swift
//  ZcashLightClientKit-Unit-Tests
//
//  Created by Francisco Gindre on 12/6/19.
//

import Foundation
@testable import ZcashLightClientKit

class MockTransactionRepository {
    enum Kind {
        case sent
        case received
    }

    var unminedCount: Int
    var receivedCount: Int
    var sentCount: Int
    var transactions: [ConfirmedTransactionEntity] = []
    var reference: [Kind] = []
    var sentTransactions: [ConfirmedTransaction] = []
    var receivedTransactions: [ConfirmedTransaction] = []
    var network: ZcashNetwork

    var allCount: Int {
        receivedCount + sentCount
    }

    init(
        unminedCount: Int,
        receivedCount: Int,
        sentCount: Int,
        network: ZcashNetwork
    ) {
        self.unminedCount = unminedCount
        self.receivedCount = receivedCount
        self.sentCount = sentCount
        self.network = network
    }

    func generate() {
        var txArray: [ConfirmedTransactionEntity] = []
        reference = referenceArray()
        for index in 0 ..< reference.count {
            txArray.append(mockTx(index: index, kind: reference[index]))
        }
        transactions = txArray
    }
    
    func referenceArray() -> [Kind] {
        var template: [Kind] = []
        
        for _ in 0 ..< sentCount {
            template.append(.sent)
        }
        for _ in 0 ..< receivedCount {
            template.append(.received)
        }

        return template.shuffled()
    }
    
    func mockTx(index: Int, kind: Kind) -> ConfirmedTransactionEntity {
        switch kind {
        case .received:
            return mockReceived(index)
        case .sent:
            return mockSent(index)
        }
    }
    
    func mockSent(_ index: Int) -> ConfirmedTransactionEntity {
        ConfirmedTransaction(
            toAddress: "some_address",
            expiryHeight: BlockHeight.max,
            minedHeight: randomBlockHeight(),
            noteId: index,
            blockTimeInSeconds: randomTimeInterval(),
            transactionIndex: index,
            raw: Data(),
            id: index,
            value: Zatoshi(Int64.random(in: 1 ... Zatoshi.Constants.oneZecInZatoshi)),
            memo: nil,
            rawTransactionId: Data()
        )
    }
    
    func mockReceived(_ index: Int) -> ConfirmedTransactionEntity {
        ConfirmedTransaction(
            toAddress: nil,
            expiryHeight: BlockHeight.max,
            minedHeight: randomBlockHeight(),
            noteId: index,
            blockTimeInSeconds: randomTimeInterval(),
            transactionIndex: index,
            raw: Data(),
            id: index,
            value: Zatoshi(Int64.random(in: 1 ... Zatoshi.Constants.oneZecInZatoshi)),
            memo: nil,
            rawTransactionId: Data()
        )
    }
    
    func randomBlockHeight() -> BlockHeight {
        BlockHeight.random(in: network.constants.saplingActivationHeight ... 1_000_000)
    }

    func randomTimeInterval() -> TimeInterval {
        Double.random(in: Date().timeIntervalSince1970 - 1000000.0 ... Date().timeIntervalSince1970)
    }
    
    func slice(txs: [ConfirmedTransactionEntity], offset: Int, limit: Int) -> [ConfirmedTransactionEntity] {
        guard offset < txs.count else { return [] }
        
        return Array(txs[offset ..< min(offset + limit, txs.count - offset)])
    }
}

extension MockTransactionRepository.Kind: Equatable {}

// MARK: - TransactionRepository
extension MockTransactionRepository: TransactionRepository {
    func countAll() throws -> Int {
        allCount
    }

    func countUnmined() throws -> Int {
        unminedCount
    }

    func blockForHeight(_ height: BlockHeight) throws -> Block? {
        nil
    }

    func findBy(id: Int) throws -> TransactionEntity? {
        transactions.first(where: { $0.id == id })?.transactionEntity
    }

    func findBy(rawId: Data) throws -> TransactionEntity? {
        transactions.first(where: { $0.rawTransactionId == rawId })?.transactionEntity
    }

    func findAllSentTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? {
        guard let indices = reference.indices(where: { $0 == .sent }) else { return nil }

        let sentTxs = indices.map { idx -> ConfirmedTransactionEntity in
            transactions[idx]
        }
        return slice(txs: sentTxs, offset: offset, limit: limit)
    }

    func findAllReceivedTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? {
        guard let indices = reference.indices(where: { $0 == .received }) else { return nil }

        let receivedTxs = indices.map { idx -> ConfirmedTransactionEntity in
            transactions[idx]
        }

        return slice(txs: receivedTxs, offset: offset, limit: limit)
    }

    func findAll(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? {
        transactions
    }

    func findAll(from: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? {
        nil
    }

    func lastScannedHeight() throws -> BlockHeight {
        return 700000
    }

    func isInitialized() throws -> Bool {
        true
    }

    func findEncodedTransactionBy(txId: Int) -> EncodedTransaction? {
        nil
    }

    func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]? {
        nil
    }

    func findConfirmedTransactionBy(rawId: Data) throws -> ConfirmedTransactionEntity? {
        nil
    }

    func findConfirmedTransactions(in range: BlockRange, offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? {
        nil
    }
}

extension Array {
    func indices(where function: (_ element: Element) -> Bool) -> [Int]? {
        guard !self.isEmpty else { return nil }

        var idx: [Int] = []

        for index in 0 ..< self.count {
            if function(self[index]) {
                idx.append(index)
            }
        }
        
        guard !idx.isEmpty else { return nil }
        return idx
    }
}
