/*
 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 runPaymentShareTest(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");

  /**
   * Case 1:
   * - Claim with first wallet and pay in the second wallet.
   * - First wallet should be notified.
   */
  {
    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,
    );

    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:14.23");
    }

    t.logStep("wait-for-payment");
    // firstWallet.waitForNotificationCond(n =>
    //   n.type === NotificationType.TransactionStateTransition &&
    //   n.transactionId === claimFirstWallet.transactionId
    // )
    // Claim the order with the first wallet
    const claimFirstWalletAgain = await firstWallet.call(
      WalletApiOperation.PreparePayForUri,
      { talerPayUri: order.uri },
    );

    t.assertTrue(
      claimFirstWalletAgain.status === PreparePayResultType.AlreadyConfirmed,
    );
    t.assertTrue(claimFirstWalletAgain.paid);

    t.logStep("w1-prepared-again");

    const r1 = await firstWallet.call(WalletApiOperation.ConfirmPay, {
      transactionId: claimFirstWallet.transactionId,
    });

    //t.assertTrue(r1.type === ConfirmPayResultType.Pending);

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

    await firstWallet.call(WalletApiOperation.TestingWaitTransactionsFinal, {});

    await secondWallet.call(
      WalletApiOperation.TestingWaitTransactionsFinal,
      {},
    );

    /**
     * only the second wallet balance was affected
     */
    {
      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("first-case-done");

  /**
   * Case 2:
   * - Claim with first wallet and share with the second wallet
   * - Pay with the first wallet, second wallet should be notified
   */
  {
    const order = await createOrder("TESTKUDOS:3");
    // 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("case2-w1-claimed");

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

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

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

    t.logStep("case2-w2-prepared");

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

    // pay from the first wallet
    const r2 = await firstWallet.call(WalletApiOperation.ConfirmPay, {
      transactionId: claimFirstWallet.transactionId,
    });

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

    // Wait for refreshes to settle before doing checks
    await firstWallet.call(WalletApiOperation.TestingWaitTransactionsFinal, {});

    /**
     * only the first wallet balance was affected
     */
    const bal1 = await firstWallet.call(WalletApiOperation.GetBalances, {});
    const bal2 = await secondWallet.call(WalletApiOperation.GetBalances, {});
    t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:16.18");
    t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:14.23");

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

    // Now the second wallet should realize that the first wallet indeed
    // paid.

    // Manually trigger checking the status.  In the future,
    // this might happen automatically via long-polling
    // in the dialog state.

    await secondWallet.call(WalletApiOperation.PreparePayForUri, {
      talerPayUri: order.uri,
    });

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

  t.logStep("second-case-done");
}

runPaymentShareTest.suites = ["wallet"];
