/*
 This file is part of GNU Taler
 (C) 2020 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

/**
 * Imports.
 */
import {
  AmountString,
  Duration,
  NotificationType,
  TransactionIdStr,
  TransactionMajorState,
  TransactionMinorState,
  j2s,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
  createSimpleTestkudosEnvironmentV3,
  withdrawViaBankV3,
} from "../harness/environments.js";
import {
  GlobalTestState,
  getTestHarnessPaytoForLabel,
} from "../harness/harness.js";

export async function runDepositMergeTest(t: GlobalTestState) {
  // Set up test environment

  const { walletClient, bankClient, exchange } =
    await createSimpleTestkudosEnvironmentV3(t);

  // Withdraw digital cash into the wallet.

  const withdrawalResult = await withdrawViaBankV3(t, {
    walletClient,
    bankClient,
    exchange,
    amount: "TESTKUDOS:50",
  });

  await withdrawalResult.withdrawalFinishedCond;

  const depositPaytoUri = getTestHarnessPaytoForLabel("foo");
  const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
  t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:49.01");

  /**
   * first deposit
   */
  let d1Id: TransactionIdStr;
  let d1Track: Promise<boolean>;
  let d1Done: Promise<boolean>;
  {
    const { transactionId: depositTxId } = await walletClient.client.call(
      WalletApiOperation.GenerateDepositGroupTxId,
      {},
    );

    d1Track = walletClient.waitForNotificationCond(
      (n) =>
        n.type == NotificationType.TransactionStateTransition &&
        n.transactionId == depositTxId &&
        n.newTxState.major == TransactionMajorState.Finalizing &&
        n.newTxState.minor == TransactionMinorState.Track,
    );

    d1Done = walletClient.waitForNotificationCond(
      (n) =>
        n.type == NotificationType.TransactionStateTransition &&
        n.transactionId == depositTxId &&
        n.newTxState.major == TransactionMajorState.Done,
    );

    const depositGroupResult = await walletClient.client.call(
      WalletApiOperation.CreateDepositGroup,
      {
        amount: "TESTKUDOS:3" as AmountString,
        depositPaytoUri,
        transactionId: depositTxId,
      },
    );

    t.assertDeepEqual(depositGroupResult.transactionId, depositTxId);
    d1Id = depositGroupResult.transactionId;
  }

  await d1Track;
  await exchange.stop();
  // @ts-ignore duration is not forever
  exchange.setTimetravel(Duration.fromSpec({ minutes: 1 }).d_ms);
  await exchange.start();
  await exchange.pingUntilAvailable();
  // total time: 1 minute

  /**
   * second deposit after 1 minute
   */
  let d2Id: TransactionIdStr;
  let d2Track: Promise<boolean>;
  let d2Done: Promise<boolean>;
  {
    const { transactionId: depositTxId } = await walletClient.client.call(
      WalletApiOperation.GenerateDepositGroupTxId,
      {},
    );

    d2Track = walletClient.waitForNotificationCond(
      (n) =>
        n.type == NotificationType.TransactionStateTransition &&
        n.transactionId == depositTxId &&
        n.newTxState.major == TransactionMajorState.Finalizing &&
        n.newTxState.minor == TransactionMinorState.Track,
    );

    d2Done = walletClient.waitForNotificationCond(
      (n) =>
        n.type == NotificationType.TransactionStateTransition &&
        n.transactionId == depositTxId &&
        n.newTxState.major == TransactionMajorState.Done,
    );

    const depositGroupResult = await walletClient.client.call(
      WalletApiOperation.CreateDepositGroup,
      {
        amount: "TESTKUDOS:3" as AmountString,
        depositPaytoUri,
        transactionId: depositTxId,
      },
    );

    t.assertDeepEqual(depositGroupResult.transactionId, depositTxId);
    d2Id = depositGroupResult.transactionId;
  }

  await d2Track;
  await exchange.stop();
  // @ts-ignore duration is not forever
  exchange.setTimetravel(Duration.fromSpec({ minutes: 2 }).d_ms);
  await exchange.start();
  await exchange.pingUntilAvailable();
  // total time: 2 minute

  /**
   * third deposit after 2 minute
   */
  let d3Id: TransactionIdStr;
  let d3Track: Promise<boolean>;
  let d3Done: Promise<boolean>;
  {
    const { transactionId: depositTxId } = await walletClient.client.call(
      WalletApiOperation.GenerateDepositGroupTxId,
      {},
    );

    d3Track = walletClient.waitForNotificationCond(
      (n) =>
        n.type == NotificationType.TransactionStateTransition &&
        n.transactionId == depositTxId &&
        n.newTxState.major == TransactionMajorState.Finalizing &&
        n.newTxState.minor == TransactionMinorState.Track,
    );

    d3Done = walletClient.waitForNotificationCond(
      (n) =>
        n.type == NotificationType.TransactionStateTransition &&
        n.transactionId == depositTxId &&
        n.newTxState.major == TransactionMajorState.Done,
    );

    const depositGroupResult = await walletClient.client.call(
      WalletApiOperation.CreateDepositGroup,
      {
        amount: "TESTKUDOS:3" as AmountString,
        depositPaytoUri,
        transactionId: depositTxId,
      },
    );

    t.assertDeepEqual(depositGroupResult.transactionId, depositTxId);
    d3Id = depositGroupResult.transactionId;
  }

  await d3Track;
  await exchange.stop();
  // @ts-ignore duration is not forever
  exchange.setTimetravel(Duration.fromSpec({ minutes: 3 }).d_ms);
  await exchange.start();
  await exchange.pingUntilAvailable();
  // total time: 3 minute

  /**
   * otherwise we get ECONNRESET in tx state because of the exchange restart
   */
  await walletClient.call(WalletApiOperation.RetryTransaction, {
    transactionId: d1Id,
  });
  await walletClient.call(WalletApiOperation.RetryTransaction, {
    transactionId: d2Id,
  });
  await walletClient.call(WalletApiOperation.RetryTransaction, {
    transactionId: d3Id,
  });

  /**
   * check deposit tx after 3 minute, all pending
   */
  {
    const d1Details = await walletClient.call(
      WalletApiOperation.GetTransactionById,
      { transactionId: d1Id },
    );

    const d2Details = await walletClient.call(
      WalletApiOperation.GetTransactionById,
      { transactionId: d2Id },
    );

    const d3Details = await walletClient.call(
      WalletApiOperation.GetTransactionById,
      { transactionId: d3Id },
    );
    console.log(j2s({ d3Details, d2Details, d1Details }));

    t.assertTrue(d1Details.txState.major === TransactionMajorState.Finalizing);
    t.assertTrue(d1Details.txState.minor === TransactionMinorState.Track);
    t.assertTrue(d2Details.txState.major === TransactionMajorState.Finalizing);
    t.assertTrue(d2Details.txState.minor === TransactionMinorState.Track);
    t.assertTrue(d3Details.txState.major === TransactionMajorState.Finalizing);
    t.assertTrue(d3Details.txState.minor === TransactionMinorState.Track);
  }

  ///////////////////////////////////////////

  await exchange.stop();
  // @ts-ignore duration is not forever
  exchange.setTimetravel(Duration.fromSpec({ minutes: 6 }).d_ms);
  await exchange.start();
  await exchange.pingUntilAvailable();
  // total time: 6 minute

  await walletClient.call(WalletApiOperation.RetryTransaction, {
    transactionId: d1Id,
  });
  await walletClient.call(WalletApiOperation.RetryTransaction, {
    transactionId: d2Id,
  });
  await walletClient.call(WalletApiOperation.RetryTransaction, {
    transactionId: d3Id,
  });

  await d1Done;
  await d2Done;
  await d3Done;
  /* check deposit tx after 6 minute, first one should already be wired since default
   * wire deadline is 5 minutes
   *
   * other deposit should already be completed since it should use the same wire transfer
   */
  {
    const d1Details = await walletClient.call(
      WalletApiOperation.GetTransactionById,
      { transactionId: d1Id },
    );
    const d2Details = await walletClient.call(
      WalletApiOperation.GetTransactionById,
      { transactionId: d2Id },
    );
    const d3Details = await walletClient.call(
      WalletApiOperation.GetTransactionById,
      { transactionId: d3Id },
    );
    console.log(j2s({ d3Details, d2Details, d1Details }));
    t.assertTrue(d1Details.txState.major === TransactionMajorState.Done);
    t.assertTrue(d2Details.txState.major === TransactionMajorState.Done);
    t.assertTrue(d3Details.txState.major === TransactionMajorState.Done);
  }
}

runDepositMergeTest.suites = ["wallet"];
