import {
  extractBitcoinRawTxVectors,
  Hex,
  BitcoinTxHash,
  BitcoinNetwork,
  ElectrumClient,
  DepositFunding,
  DepositScript,
  BitcoinAddressConverter,
  WalletTx,
  BitcoinHashUtils,
  EthereumAddress,
  TBTC,
} from "@keep-network/tbtc-v2.ts"
import { BigNumber, constants, Contract } from "ethers"
import chai, { expect } from "chai"
import chaiAsPromised from "chai-as-promised"

import {
  setupSystemTestsContext,
  createTbtcContractsHandle,
} from "./utils/context"
import { fakeRelayDifficulty, waitTransactionConfirmed } from "./utils/bitcoin"

import type {
  RedemptionRequest,
  BitcoinUtxo,
  DepositReceipt,
} from "@keep-network/tbtc-v2.ts"
import type { SystemTestsContext } from "./utils/context"

chai.use(chaiAsPromised)

/**
 * This system test scenario performs a single deposit and redemption.
 *
 * The scenario consists of the following steps:
 * 1. The depositor broadcasts the deposit transaction on BTC chain and reveals
 *    it to the bridge.
 * 2. The wallet broadcasts the sweep transaction of the given deposit on BTC
 *    chain and submits the sweep proof to the bridge.
 * 3. The depositor (redeemer) requests the redemption of its entire bank
 *    balance.
 * 4. The wallet broadcasts the redemption transaction handling the given
 *    request and submits the redemption proof to the bridge.
 *
 * Following prerequisites must be fulfilled to make a successful pass:
 * - The depositor's BTC balance must allow to perform the deposit
 * - tBTC v2 contracts must be deployed on used Ethereum network
 * - A fresh live wallet (with no main UTXO yet) must be registered in
 *   the bridge
 */
describe("System Test - Deposit and redemption", () => {
  let systemTestsContext: SystemTestsContext

  let bridgeAddress: string
  let bank: Contract
  let relay: Contract

  let depositorSdk: TBTC
  let maintainerSdk: TBTC
  let walletTx: WalletTx

  const depositAmount = BigNumber.from(2000000)
  const depositSweepTxFee = BigNumber.from(10000)
  const depositTxFee = BigNumber.from(1500)
  // Number of retries for Electrum requests.
  const ELECTRUM_RETRIES = 5
  // Initial backoff step in milliseconds that will be increased exponentially for
  // subsequent Electrum retry attempts.
  const ELECTRUM_RETRY_BACKOFF_STEP_MS = 10000 // 10sec

  let depositReceipt: DepositReceipt
  let depositUtxo: BitcoinUtxo
  let sweepUtxo: BitcoinUtxo

  let depositorBitcoinAddress: string

  before(async () => {
    systemTestsContext = await setupSystemTestsContext()
    const { electrumUrl, maintainer, depositor, deployedContracts } =
      systemTestsContext

    const relayDeploymentInfo = deployedContracts.LightRelay
    relay = new Contract(
      relayDeploymentInfo.address,
      relayDeploymentInfo.abi,
      maintainer
    )

    const bankDeploymentInfo = deployedContracts.Bank
    bank = new Contract(
      bankDeploymentInfo.address,
      bankDeploymentInfo.abi,
      maintainer
    )

    const electrumClient = ElectrumClient.fromUrl(
      electrumUrl,
      undefined,
      ELECTRUM_RETRIES,
      ELECTRUM_RETRY_BACKOFF_STEP_MS
    )

    bridgeAddress = deployedContracts.Bridge.address

    const depositorTbtcContracts = await createTbtcContractsHandle(
      deployedContracts,
      depositor
    )

    depositorSdk = await TBTC.initializeCustom(
      depositorTbtcContracts,
      electrumClient
    )

    const maintainerTbtcContracts = await createTbtcContractsHandle(
      deployedContracts,
      maintainer
    )

    maintainerSdk = await TBTC.initializeCustom(
      maintainerTbtcContracts,
      electrumClient
    )

    walletTx = new WalletTx(maintainerTbtcContracts, electrumClient)

    depositorBitcoinAddress = BitcoinAddressConverter.publicKeyToAddress(
      systemTestsContext.depositorBitcoinKeyPair.publicKey.compressed,
      BitcoinNetwork.Testnet
    )

    depositorSdk.deposits.setDefaultDepositor(
      EthereumAddress.from(await depositor.getAddress())
    )
  })

  context("when deposit is made and revealed", () => {
    before("make and reveal deposit", async () => {
      const deposit = await depositorSdk.deposits.initiateDeposit(
        // Use the depositor's address as the recovery address.
        depositorBitcoinAddress
      )
      depositReceipt = deposit.getReceipt()
      const depositScript = DepositScript.fromReceipt(depositReceipt)
      const depositFunding = DepositFunding.fromScript(depositScript)

      console.log(`
        Deposit receipt generated:
        - depositor: ${depositReceipt.depositor.identifierHex}
        - walletPublicKeyHash: ${depositReceipt.walletPublicKeyHash}
        - refundPublicKeyHash: ${depositReceipt.refundPublicKeyHash}
        - blindingFactor: ${depositReceipt.blindingFactor}
        - refundLocktime: ${depositReceipt.refundLocktime}
      `)

      const depositorUtxos =
        await depositorSdk.bitcoinClient.findAllUnspentTransactionOutputs(
          depositorBitcoinAddress
        )

      ;({ depositUtxo } = await depositFunding.submitTransaction(
        depositAmount,
        depositorUtxos,
        depositTxFee,
        systemTestsContext.depositorBitcoinKeyPair.wif,
        depositorSdk.bitcoinClient
      ))

      console.log(`
        Deposit made on BTC chain:
        - Transaction hash: ${depositUtxo.transactionHash}
        - Output index: ${depositUtxo.outputIndex}
      `)

      // Since the reveal deposit logic does not perform SPV proof, we
      // can reveal the deposit transaction immediately without waiting
      // for confirmations.
      const rawDepositTransaction =
        await depositorSdk.bitcoinClient.getRawTransaction(
          depositUtxo.transactionHash
        )
      const depositRawTxVectors = extractBitcoinRawTxVectors(
        rawDepositTransaction
      )

      // Reveal without providing the vault address.
      await depositorSdk.tbtcContracts.bridge.revealDeposit(
        depositRawTxVectors,
        depositUtxo.outputIndex,
        depositReceipt
      )

      console.log(`
        Deposit revealed on Ethereum chain
      `)
    })

    it("should broadcast the deposit transaction on the Bitcoin network", async () => {
      expect(
        (
          await maintainerSdk.bitcoinClient.getRawTransaction(
            depositUtxo.transactionHash
          )
        ).transactionHex.length
      ).to.be.greaterThan(0)
    })

    it("should reveal the deposit to the bridge", async () => {
      const { revealedAt } = await maintainerSdk.tbtcContracts.bridge.deposits(
        depositUtxo.transactionHash,
        depositUtxo.outputIndex
      )
      expect(revealedAt).to.be.greaterThan(0)
    })

    context("when deposit is swept and sweep proof submitted", () => {
      before("sweep the deposit and submit sweep proof", async () => {
        ;({ newMainUtxo: sweepUtxo } =
          await walletTx.depositSweep.submitTransaction(
            depositSweepTxFee,
            systemTestsContext.walletBitcoinKeyPair.wif,
            [depositUtxo],
            [depositReceipt]
          ))

        console.log(`
        Deposit swept on Bitcoin chain:
        - Transaction hash: ${sweepUtxo.transactionHash}
      `)

        // Unlike in the deposit transaction case, we must wait for the sweep
        // transaction to have an enough number of confirmations. This is
        // because the bridge performs the SPV proof of that transaction.
        await waitTransactionConfirmed(
          maintainerSdk.bitcoinClient,
          sweepUtxo.transactionHash
        )

        await fakeRelayDifficulty(
          relay,
          maintainerSdk.bitcoinClient,
          sweepUtxo.transactionHash
        )

        // TODO: Consider fetching the current wallet main UTXO and passing it
        //       here. This will allow running this test scenario multiple
        //       times for the same wallet.
        await maintainerSdk.maintenance.spv.submitDepositSweepProof(
          sweepUtxo.transactionHash,
          // This is the first sweep of the given wallet so there is no main UTXO.
          {
            // The function expects an unprefixed hash.
            transactionHash: BitcoinTxHash.from(constants.HashZero),
            outputIndex: 0,
            value: BigNumber.from(0),
          }
        )

        console.log(`
        Deposit sweep proved on the bridge
      `)
      })

      it("should broadcast the sweep transaction on the Bitcoin network", async () => {
        expect(
          (
            await maintainerSdk.bitcoinClient.getRawTransaction(
              sweepUtxo.transactionHash
            )
          ).transactionHex.length
        ).to.be.greaterThan(0)
      })

      it("should sweep the deposit on the bridge", async () => {
        const { sweptAt } = await maintainerSdk.tbtcContracts.bridge.deposits(
          depositUtxo.transactionHash,
          depositUtxo.outputIndex
        )
        expect(sweptAt).to.be.greaterThan(0)
      })

      it("should increase depositor's balance in the bank", async () => {
        const { treasuryFee } =
          await maintainerSdk.tbtcContracts.bridge.deposits(
            depositUtxo.transactionHash,
            depositUtxo.outputIndex
          )

        const expectedBalance = depositAmount
          .sub(treasuryFee)
          .sub(depositSweepTxFee)

        const actualBalance = await bank.balanceOf(
          systemTestsContext.depositor.address
        )

        expect(actualBalance).to.be.equal(expectedBalance)
      })

      context("when redemption is requested", () => {
        let requestedAmount: BigNumber
        let redeemerOutputScript: Hex
        let redemptionRequest: RedemptionRequest

        before("request the redemption", async () => {
          // Redeem the full depositor's balance.
          requestedAmount = await bank.balanceOf(
            systemTestsContext.depositor.address
          )

          // Allow the bridge to take the redeemed bank balance.
          await bank
            .connect(systemTestsContext.depositor)
            .approveBalance(bridgeAddress, requestedAmount)

          // Request redemption to depositor's address.
          redeemerOutputScript = Hex.from(
            `0014${BitcoinHashUtils.computeHash160(
              systemTestsContext.depositorBitcoinKeyPair.publicKey.compressed
            )}`
          )

          await depositorSdk.tbtcContracts.bridge.requestRedemption(
            systemTestsContext.walletBitcoinKeyPair.publicKey.compressed,
            sweepUtxo,
            redeemerOutputScript,
            requestedAmount
          )

          console.log(
            `Requested redemption of ${requestedAmount} satoshis to script ${redeemerOutputScript} on the bridge`
          )

          redemptionRequest =
            await maintainerSdk.redemptions.getRedemptionRequests(
              depositorBitcoinAddress,
              systemTestsContext.walletBitcoinKeyPair.publicKey.compressed,
              "pending"
            )
        })

        it("should transfer depositor's bank balance to the Bridge", async () => {
          expect(
            await bank.balanceOf(systemTestsContext.depositor.address)
          ).to.be.equal(0)

          expect(await bank.balanceOf(bridgeAddress)).to.be.equal(
            requestedAmount
          )
        })

        it("should register the redemption request on the bridge", async () => {
          expect(redemptionRequest.requestedAt).to.be.greaterThan(0)
          expect(redemptionRequest.requestedAmount).to.be.equal(requestedAmount)
          expect(redemptionRequest.redeemerOutputScript).to.be.deep.equal(
            redeemerOutputScript
          )
        })

        context(
          "when redemption is made and redemption proof submitted",
          () => {
            let redemptionTxHash: BitcoinTxHash

            before(
              "make the redemption and submit redemption proof",
              async () => {
                ;({ transactionHash: redemptionTxHash } =
                  await walletTx.redemption.submitTransaction(
                    systemTestsContext.walletBitcoinKeyPair.wif,
                    sweepUtxo,
                    [redemptionRequest.redeemerOutputScript]
                  ))

                console.log(
                  "Redemption made on Bitcoin chain:\n" +
                    `- Transaction hash: ${redemptionTxHash}`
                )

                await waitTransactionConfirmed(
                  maintainerSdk.bitcoinClient,
                  redemptionTxHash
                )

                await fakeRelayDifficulty(
                  relay,
                  maintainerSdk.bitcoinClient,
                  redemptionTxHash
                )

                await maintainerSdk.maintenance.spv.submitRedemptionProof(
                  redemptionTxHash,
                  sweepUtxo,
                  systemTestsContext.walletBitcoinKeyPair.publicKey.compressed
                )

                console.log("Redemption proved on the bridge")
              }
            )

            it("should broadcast the redemption transaction on the Bitcoin network", async () => {
              expect(
                (
                  await maintainerSdk.bitcoinClient.getRawTransaction(
                    redemptionTxHash
                  )
                ).transactionHex.length
              ).to.be.greaterThan(0)
            })

            it("should close the redemption request on the bridge", async () => {
              await expect(
                maintainerSdk.redemptions.getRedemptionRequests(
                  depositorBitcoinAddress,
                  systemTestsContext.walletBitcoinKeyPair.publicKey.compressed,
                  "pending"
                )
              ).to.be.rejectedWith("Redemption request does not exist")
            })

            it("should decrease Bridge's balance in the bank", async () => {
              const actualBalance = await bank.balanceOf(bridgeAddress)

              expect(actualBalance).to.be.equal(0)
            })
          }
        )
      })
    })
  })
})