/*
 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 {
  AbsoluteTime,
  ChoiceSelectionDetailType,
  Duration,
  Order,
  OrderInputType,
  OrderOutputType,
  PreparePayResultType,
  succeedOrThrow,
  TalerMerchantInstanceHttpClient,
  TalerProtocolTimestamp,
  TokenAvailabilityHint,
  TokenFamilyKind,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
  createSimpleTestkudosEnvironmentV3,
  withdrawViaBankV3,
} from "harness/environments.js";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import { GlobalTestState } from "../harness/harness.js";
import { logger } from "./test-tops-challenger-twice.js";

export async function runWalletTokensTest(t: GlobalTestState) {
  let {
    bankClient,
    exchange,
    merchant,
    walletClient,
    merchantAdminAccessToken,
  } = await createSimpleTestkudosEnvironmentV3(
    t,
    defaultCoinConfig.map((x) => x("TESTKUDOS")),
    {
      walletConfig: {
        features: {
          enableV1Contracts: true,
        },
      },
    },
  );

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

  // withdraw some test money
  const wres = await withdrawViaBankV3(t, {
    walletClient,
    bankClient,
    exchange,
    amount: "TESTKUDOS:40",
  });
  await wres.withdrawalFinishedCond;

  // setup discount token family
  succeedOrThrow(
    await merchantApi.createTokenFamily(merchantAdminAccessToken, {
      kind: TokenFamilyKind.Discount,
      slug: "test_discount",
      name: "Test discount",
      description: "This is a test discount",
      valid_after: TalerProtocolTimestamp.now(),
      valid_before: AbsoluteTime.toProtocolTimestamp(
        AbsoluteTime.addDuration(
          AbsoluteTime.now(),
          Duration.fromSpec({ years: 1 }),
        ),
      ),
      duration: Duration.toTalerProtocolDuration(
        Duration.fromSpec({ days: 90 }),
      ),
      validity_granularity: Duration.toTalerProtocolDuration(
        Duration.fromSpec({ days: 1 }),
      ),
    }),
  );

  // setup subscription token family
  succeedOrThrow(
    await merchantApi.createTokenFamily(merchantAdminAccessToken, {
      kind: TokenFamilyKind.Subscription,
      slug: "test_subscription",
      name: "Test subscription",
      description: "This is a test subscription",
      valid_after: TalerProtocolTimestamp.now(),
      valid_before: AbsoluteTime.toProtocolTimestamp(
        AbsoluteTime.addDuration(
          AbsoluteTime.now(),
          Duration.fromSpec({ years: 1 }),
        ),
      ),
      duration: Duration.toTalerProtocolDuration(
        Duration.fromSpec({ days: 90 }),
      ),
      validity_granularity: Duration.toTalerProtocolDuration(
        Duration.fromSpec({ days: 1 }),
      ),
    }),
  );

  let orderJsonDiscount: Order = {
    version: 1,
    summary: "Test order",
    timestamp: TalerProtocolTimestamp.now(),
    pay_deadline: AbsoluteTime.toProtocolTimestamp(
      AbsoluteTime.addDuration(
        AbsoluteTime.now(),
        Duration.fromSpec({ days: 1 }),
      ),
    ),
    choices: [
      {
        amount: "TESTKUDOS:2",
        inputs: [],
        outputs: [
          {
            type: OrderOutputType.Token,
            token_family_slug: "test_discount",
          },
        ],
      },
      {
        amount: "TESTKUDOS:1",
        inputs: [
          {
            type: OrderInputType.Token,
            token_family_slug: "test_discount",
          },
        ],
        outputs: [],
      },
    ],
  };

  let orderJsonSubscription: Order = {
    version: 1,
    summary: "Test order",
    timestamp: TalerProtocolTimestamp.now(),
    pay_deadline: AbsoluteTime.toProtocolTimestamp(
      AbsoluteTime.addDuration(
        AbsoluteTime.now(),
        Duration.fromSpec({ days: 1 }),
      ),
    ),
    choices: [
      {
        amount: "TESTKUDOS:10",
        inputs: [],
        outputs: [
          {
            type: OrderOutputType.Token,
            token_family_slug: "test_subscription",
            count: 1,
          },
        ],
      },
      {
        amount: "TESTKUDOS:0",
        inputs: [
          {
            type: OrderInputType.Token,
            token_family_slug: "test_subscription",
            count: 1,
          },
        ],
        outputs: [
          {
            type: OrderOutputType.Token,
            token_family_slug: "test_subscription",
            count: 1,
          },
        ],
      },
    ],
  };

  {
    logger.info("Payment with discount token output...");
    const choiceIndex = 0;

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

    let orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(
        merchantAdminAccessToken,
        orderResp.order_id,
      ),
    );

    t.assertTrue(orderStatus.order_status === "unpaid");

    const talerPayUri = orderStatus.taler_pay_uri;
    const orderId = orderResp.order_id;

    const preparePayResult = await walletClient.call(
      WalletApiOperation.PreparePayForUri,
      {
        talerPayUri,
      },
    );

    t.assertTrue(
      preparePayResult.status === PreparePayResultType.ChoiceSelection,
    );

    await walletClient.call(WalletApiOperation.ConfirmPay, {
      transactionId: preparePayResult.transactionId,
      choiceIndex,
    });

    orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(merchantAdminAccessToken, orderId),
    );

    t.assertTrue(orderStatus.order_status === "paid");
  }

  {
    logger.info("Payment with discount token input...");
    const choiceIndex = 1;

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

    let orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(
        merchantAdminAccessToken,
        orderResp.order_id,
      ),
    );

    t.assertTrue(orderStatus.order_status === "unpaid");

    const talerPayUri = orderStatus.taler_pay_uri;
    const orderId = orderResp.order_id;

    const preparePayResult = await walletClient.call(
      WalletApiOperation.PreparePayForUri,
      {
        talerPayUri,
      },
    );

    t.assertTrue(
      preparePayResult.status === PreparePayResultType.ChoiceSelection,
    );

    {
      const choicesRes = await walletClient.call(
        WalletApiOperation.GetChoicesForPayment,
        {
          transactionId: preparePayResult.transactionId,
        },
      );

      t.assertTrue(choicesRes.defaultChoiceIndex === 1);
      t.assertTrue(choicesRes.automaticExecution === false);
      t.assertTrue(choicesRes.automaticExecutableIndex === undefined);

      t.assertTrue(
        choicesRes.choices[choiceIndex].status ===
          ChoiceSelectionDetailType.PaymentPossible,
      );

      const tokenDetails = choicesRes.choices[choiceIndex].tokenDetails;
      logger.info("tokenDetails:", tokenDetails);
      t.assertTrue(tokenDetails?.tokensAvailable === 1);
      t.assertTrue(tokenDetails?.tokensRequested === 1);
      for (const tf in tokenDetails?.perTokenFamily ?? t.fail()) {
        t.assertTrue(tokenDetails?.perTokenFamily[tf].available === 1);
        t.assertTrue(tokenDetails?.perTokenFamily[tf].requested === 1);
        t.assertTrue(tokenDetails?.perTokenFamily[tf].causeHint === undefined);
        break;
      }
    }

    await walletClient.call(WalletApiOperation.ConfirmPay, {
      transactionId: preparePayResult.transactionId,
      choiceIndex,
    });

    orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(merchantAdminAccessToken, orderId),
    );
  }

  {
    logger.info("Payment with discount token input, insufficient tokens...");
    const choiceIndex = 1;

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

    let orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(
        merchantAdminAccessToken,
        orderResp.order_id,
      ),
    );

    t.assertTrue(orderStatus.order_status === "unpaid");

    const talerPayUri = orderStatus.taler_pay_uri;
    const orderId = orderResp.order_id;

    const preparePayResult = await walletClient.call(
      WalletApiOperation.PreparePayForUri,
      {
        talerPayUri,
      },
    );

    t.assertTrue(
      preparePayResult.status === PreparePayResultType.ChoiceSelection,
    );

    {
      const choicesRes = await walletClient.call(
        WalletApiOperation.GetChoicesForPayment,
        {
          transactionId: preparePayResult.transactionId,
        },
      );

      t.assertTrue(choicesRes.defaultChoiceIndex === 0);
      t.assertTrue(choicesRes.automaticExecution === false);
      t.assertTrue(choicesRes.automaticExecutableIndex === undefined);

      t.assertTrue(
        choicesRes.choices[choiceIndex].status ===
          ChoiceSelectionDetailType.InsufficientBalance,
      );

      const tokenDetails = choicesRes.choices[choiceIndex].tokenDetails;
      logger.info("tokenDetails:", tokenDetails);
      t.assertTrue(tokenDetails?.tokensAvailable === 0);
      t.assertTrue(tokenDetails?.tokensRequested === 1);
      for (const tf in tokenDetails?.perTokenFamily ?? t.fail()) {
        t.assertTrue(tokenDetails?.perTokenFamily[tf].available === 0);
        t.assertTrue(tokenDetails?.perTokenFamily[tf].requested === 1);
        t.assertTrue(
          tokenDetails?.perTokenFamily[tf].causeHint ===
            TokenAvailabilityHint.WalletTokensAvailableInsufficient,
        );
        break;
      }
    }

    // should fail because we have no tokens left
    t.assertThrowsAsync(async () => {
      await walletClient.call(WalletApiOperation.ConfirmPay, {
        transactionId: preparePayResult.transactionId,
        choiceIndex,
      });
    });

    orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(merchantAdminAccessToken, orderId),
    );

    t.assertTrue(orderStatus.order_status === "claimed");
  }

  {
    logger.info(
      "Payment with subscription token input and output, insufficient balance...",
    );
    const choiceIndex = 1;

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

    let orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(
        merchantAdminAccessToken,
        orderResp.order_id,
      ),
    );

    t.assertTrue(orderStatus.order_status === "unpaid");

    const talerPayUri = orderStatus.taler_pay_uri;
    const orderId = orderResp.order_id;

    const preparePayResult = await walletClient.call(
      WalletApiOperation.PreparePayForUri,
      {
        talerPayUri,
      },
    );

    t.assertTrue(
      preparePayResult.status === PreparePayResultType.ChoiceSelection,
    );

    {
      const choicesRes = await walletClient.call(
        WalletApiOperation.GetChoicesForPayment,
        {
          transactionId: preparePayResult.transactionId,
        },
      );

      t.assertTrue(choicesRes.defaultChoiceIndex === 0);
      t.assertTrue(choicesRes.automaticExecution === false);
      t.assertTrue(choicesRes.automaticExecutableIndex === choiceIndex);

      t.assertTrue(
        choicesRes.choices[choiceIndex].status ===
          ChoiceSelectionDetailType.InsufficientBalance,
      );

      const tokenDetails = choicesRes.choices[choiceIndex].tokenDetails;
      logger.info("tokenDetails:", tokenDetails);
      t.assertTrue(tokenDetails?.tokensAvailable === 0);
      t.assertTrue(tokenDetails?.tokensRequested === 1);
      for (const tf in tokenDetails?.perTokenFamily ?? t.fail()) {
        t.assertTrue(tokenDetails?.perTokenFamily[tf].available === 0);
        t.assertTrue(tokenDetails?.perTokenFamily[tf].requested === 1);
        t.assertTrue(
          tokenDetails?.perTokenFamily[tf].causeHint ===
            TokenAvailabilityHint.WalletTokensAvailableInsufficient,
        );
        break;
      }
    }
  }

  {
    logger.info("Payment with subscription token output...");
    const choiceIndex = 0;

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

    let orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(
        merchantAdminAccessToken,
        orderResp.order_id,
      ),
    );

    t.assertTrue(orderStatus.order_status === "unpaid");

    const talerPayUri = orderStatus.taler_pay_uri;
    const orderId = orderResp.order_id;

    const preparePayResult = await walletClient.call(
      WalletApiOperation.PreparePayForUri,
      {
        talerPayUri,
      },
    );

    t.assertTrue(
      preparePayResult.status === PreparePayResultType.ChoiceSelection,
    );

    await walletClient.call(WalletApiOperation.ConfirmPay, {
      transactionId: preparePayResult.transactionId,
      choiceIndex,
    });

    orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(merchantAdminAccessToken, orderId),
    );

    t.assertTrue(orderStatus.order_status === "paid");
  }

  {
    logger.info("Payment with subscription token input and output...");
    const choiceIndex = 1;

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

    let orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(
        merchantAdminAccessToken,
        orderResp.order_id,
      ),
    );

    t.assertTrue(orderStatus.order_status === "unpaid");

    const talerPayUri = orderStatus.taler_pay_uri;
    const orderId = orderResp.order_id;

    const preparePayResult = await walletClient.call(
      WalletApiOperation.PreparePayForUri,
      {
        talerPayUri,
      },
    );

    t.assertTrue(
      preparePayResult.status === PreparePayResultType.ChoiceSelection,
    );

    {
      const choicesRes = await walletClient.call(
        WalletApiOperation.GetChoicesForPayment,
        {
          transactionId: preparePayResult.transactionId,
        },
      );

      t.assertTrue(choicesRes.defaultChoiceIndex === choiceIndex);
      t.assertTrue(choicesRes.automaticExecution === true);
      t.assertTrue(choicesRes.automaticExecutableIndex === choiceIndex);

      t.assertTrue(
        choicesRes.choices[choiceIndex].status ===
          ChoiceSelectionDetailType.PaymentPossible,
      );

      const tokenDetails = choicesRes.choices[choiceIndex].tokenDetails;
      logger.info("tokenDetails:", tokenDetails);
      t.assertTrue(tokenDetails?.tokensAvailable === 1);
      t.assertTrue(tokenDetails?.tokensRequested === 1);
      for (const tf in tokenDetails?.perTokenFamily ?? t.fail()) {
        t.assertTrue(tokenDetails?.perTokenFamily[tf].available === 1);
        t.assertTrue(tokenDetails?.perTokenFamily[tf].requested === 1);
        t.assertTrue(tokenDetails?.perTokenFamily[tf].causeHint === undefined);
        break;
      }
    }

    await walletClient.call(WalletApiOperation.ConfirmPay, {
      transactionId: preparePayResult.transactionId,
      choiceIndex,
    });

    orderStatus = succeedOrThrow(
      await merchantApi.getOrderDetails(merchantAdminAccessToken, orderId),
    );
  }
}

runWalletTokensTest.suites = ["merchant", "wallet"];
