/*
 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,
  ConfirmPayResultType,
  PreparePayResultType,
  succeedOrThrow,
  TalerMerchantInstanceHttpClient,
  TransactionMajorState,
  TransactionMinorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
  createSimpleTestkudosEnvironmentV3,
  createWalletDaemonWithClient,
  withdrawViaBankV3,
} from "../harness/environments.js";
import { GlobalTestState } from "../harness/harness.js";

/**
 * Run test for basic, bank-integrated withdrawal and payment.
 */
export async function runPaymentShareIdempotencyTest(t: GlobalTestState) {
  // Set up test environment
  const {
    walletClient: firstWallet,
    bankClient,
    exchange,
    merchant,
    merchantAdminAccessToken
  } = await createSimpleTestkudosEnvironmentV3(t);

  const merchantClient = new TalerMerchantInstanceHttpClient(
    merchant.makeInstanceBaseUrl(),
  );

  // Withdraw digital cash into the wallet.
  await withdrawViaBankV3(t, {
    walletClient: firstWallet,
    bankClient,
    exchange,
    amount: "TESTKUDOS:20",
  });
  await firstWallet.call(WalletApiOperation.TestingWaitTransactionsFinal, {});

  const { walletClient: secondWallet } = await createWalletDaemonWithClient(t, {
    name: "wallet2",
  });

  await withdrawViaBankV3(t, {
    walletClient: secondWallet,
    bankClient,
    exchange,
    amount: "TESTKUDOS:20",
  });
  await secondWallet.call(WalletApiOperation.TestingWaitTransactionsFinal, {});

  {
    const first = await firstWallet.call(WalletApiOperation.GetBalances, {});
    const second = await secondWallet.call(WalletApiOperation.GetBalances, {});
    t.assertAmountEquals(first.balances[0].available, "TESTKUDOS:19.53");
    t.assertAmountEquals(second.balances[0].available, "TESTKUDOS:19.53");
  }

  t.logStep("setup-done");

  // create two orders to pay
  async function createOrder(amount: string) {
    const order = {
      summary: "Buy me!",
      amount: amount as AmountString,
      fulfillment_url: "taler://fulfillment-success/thx",
    };

    const args = { order };

    const orderResp = succeedOrThrow(
      await merchantClient.createOrder(merchantAdminAccessToken, {
        order: args.order,
      }),
    );

    const orderStatus = succeedOrThrow(
      await merchantClient.getOrderDetails(merchantAdminAccessToken, orderResp.order_id),
    );

    t.assertTrue(orderStatus.order_status === "unpaid");
    return { id: orderResp.order_id, uri: orderStatus.taler_pay_uri };
  }

  t.logStep("orders-created");

  /**
   * Scenario A (a)
   * - Claim with first wallet, don't confirm.
   * - Claim shared URI with second wallet, don't confirm.
   * - Claim with second wallet again, should still be able to confirm.
   */
  {
    const order = await createOrder("TESTKUDOS:5");
    // Claim the order with the first wallet
    const claimFirstWallet = await firstWallet.call(
      WalletApiOperation.PreparePayForUri,
      { talerPayUri: order.uri },
    );

    t.assertTrue(
      claimFirstWallet.status === PreparePayResultType.PaymentPossible,
    );

    t.logStep("w1-payment-possible");

    // share order from the first wallet
    const { privatePayUri } = await firstWallet.call(
      WalletApiOperation.SharePayment,
      {
        merchantBaseUrl: merchant.makeInstanceBaseUrl(),
        orderId: order.id,
      },
    );

    t.logStep("w1-payment-shared");

    // claim from the second wallet
    const claimSecondWallet = await secondWallet.call(
      WalletApiOperation.PreparePayForUri,
      { talerPayUri: privatePayUri },
    );

    t.assertTrue(
      claimSecondWallet.status === PreparePayResultType.PaymentPossible,
    );

    await secondWallet.call(WalletApiOperation.TestingWaitTransactionState, {
      transactionId: claimSecondWallet.transactionId,
      txState: {
        major: TransactionMajorState.Dialog,
        minor: TransactionMinorState.MerchantOrderProposed,
      },
    });

    t.logStep("w2-claimed");

    // claim from the second wallet (again)
    const claimSecondWalletAgain = await secondWallet.call(
      WalletApiOperation.PreparePayForUri,
      { talerPayUri: privatePayUri },
    );

    t.assertTrue(
      claimSecondWalletAgain.status === PreparePayResultType.PaymentPossible,
    );

    t.logStep("w2-claimed-again");

    // pay from the second wallet
    const r2 = await secondWallet.call(WalletApiOperation.ConfirmPay, {
      transactionId: claimSecondWalletAgain.transactionId,
    });

    t.assertTrue(r2.type === ConfirmPayResultType.Done);

    t.logStep("w2-confirmed");

    // Wait for refresh to settle before we do checks
    await secondWallet.call(
      WalletApiOperation.TestingWaitTransactionsFinal,
      {},
    );

    t.logStep("w2-refresh-settled");

    {
      const first = await firstWallet.call(WalletApiOperation.GetBalances, {});
      const second = await secondWallet.call(
        WalletApiOperation.GetBalances,
        {},
      );
      t.assertAmountEquals(first.balances[0].available, "TESTKUDOS:19.53");
      t.assertAmountEquals(second.balances[0].available, "TESTKUDOS:14.23");
    }

    t.logStep("wait-for-payment");
  }

  t.logStep("scenario-a-a-done");

  /**
   * Scenario A (b)
   * - Claim with first wallet, don't confirm.
   * - Claim shared URI with second wallet, confirm.
   * - Claim with second wallet again, tx should transition to `done`.
   */
  {
    const order = await createOrder("TESTKUDOS:5");
    // Claim the order with the first wallet
    const claimFirstWallet = await firstWallet.call(
      WalletApiOperation.PreparePayForUri,
      { talerPayUri: order.uri },
    );

    t.assertTrue(
      claimFirstWallet.status === PreparePayResultType.PaymentPossible,
    );

    t.logStep("w1-payment-possible");

    // share order from the first wallet
    const { privatePayUri } = await firstWallet.call(
      WalletApiOperation.SharePayment,
      {
        merchantBaseUrl: merchant.makeInstanceBaseUrl(),
        orderId: order.id,
      },
    );

    t.logStep("w1-payment-shared");

    // claim from the second wallet
    const claimSecondWallet = await secondWallet.call(
      WalletApiOperation.PreparePayForUri,
      { talerPayUri: privatePayUri },
    );

    console.log(`second claim tx id: ${claimSecondWallet.transactionId}`);

    t.assertTrue(
      claimSecondWallet.status === PreparePayResultType.PaymentPossible,
    );

    t.logStep("w2-claimed");

    // pay from the second wallet
    const r2 = await secondWallet.call(WalletApiOperation.ConfirmPay, {
      transactionId: claimSecondWallet.transactionId,
    });

    t.assertTrue(r2.type === ConfirmPayResultType.Done);

    t.logStep("w2-confirmed");

    // Wait for refresh to settle before we do checks
    await secondWallet.call(
      WalletApiOperation.TestingWaitTransactionsFinal,
      {},
    );

    t.logStep("w2-refresh-settled");

    {
      const first = await firstWallet.call(WalletApiOperation.GetBalances, {});
      const second = await secondWallet.call(
        WalletApiOperation.GetBalances,
        {},
      );
      t.assertAmountEquals(first.balances[0].available, "TESTKUDOS:19.53");
      t.assertAmountEquals(second.balances[0].available, "TESTKUDOS:8.95");
    }

    t.logStep("wait-for-payment");

    // claim from the second wallet (again)
    const claimSecondWalletAgain = await secondWallet.call(
      WalletApiOperation.PreparePayForUri,
      { talerPayUri: privatePayUri },
    );

    t.assertTrue(
      claimSecondWalletAgain.status === PreparePayResultType.AlreadyConfirmed,
    );

    t.logStep("scenario-a-b-wait-done");

    await secondWallet.call(WalletApiOperation.TestingWaitTransactionState, {
      transactionId: claimSecondWalletAgain.transactionId,
      txState: {
        major: TransactionMajorState.Done,
      },
    });
  }

  t.logStep("scenario-a-b-done");
}

runPaymentShareIdempotencyTest.suites = ["wallet"];
