/*
 This file is part of GNU Taler
 (C) 2024 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 {
  Configuration,
  createEddsaKeyPair,
  encodeCrock,
  getRandomBytes,
  hashNormalizedPaytoUri,
  HttpStatusCode,
  j2s,
  TalerWireGatewayHttpClient,
  TransactionMajorState,
  TransactionMinorState,
} from "@gnu-taler/taler-util";
import {
  createSyncCryptoApi,
  WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
import {
  configureCommonKyc,
  createKycTestkudosEnvironment,
  postAmlDecisionNoRules,
  withdrawViaBankV3,
} from "../harness/environments.js";
import {
  getTestHarnessPaytoForLabel,
  GlobalTestState,
} from "../harness/harness.js";

const myAmlConfig = `
# Fallback measure on errors.
[kyc-measure-freeze-investigate]
CHECK_NAME = skip
PROGRAM = freeze-investigate
VOLUNTARY = NO
CONTEXT = {}

[aml-program-freeze-investigate]
DESCRIPTION = "Fallback measure on errors that freezes the account and asks AML staff to investigate the system failure."
COMMAND = taler-exchange-helper-measure-freeze
ENABLED = YES
FALLBACK = freeze-investigate

[aml-program-inform-investigate]
DESCRIPTION = "Measure that asks AML staff to investigate an account and informs the account owner about it."
COMMAND = taler-exchange-helper-measure-inform-investigate
ENABLED = YES
FALLBACK = freeze-investigate

[kyc-check-form-gls-merchant-onboarding]
TYPE = FORM
FORM_NAME = gls-merchant-onboarding
DESCRIPTION = "GLS Merchant Onboarding"
DESCRIPTION_I18N = {}
OUTPUTS =
FALLBACK = freeze-investigate

[kyc-measure-merchant-onboarding]
CHECK_NAME = form-gls-merchant-onboarding
PROGRAM = inform-investigate
CONTEXT = {}
VOLUNTARY = NO

[kyc-rule-deposit-limit-zero]
OPERATION_TYPE = DEPOSIT
NEXT_MEASURES = merchant-onboarding
EXPOSED = YES
ENABLED = YES
THRESHOLD = TESTKUDOS:1
TIMEFRAME = "1 days"
`;

function adjustExchangeConfig(config: Configuration) {
  configureCommonKyc(config);
  config.loadFromString(myAmlConfig);
}

/**
 * Test for KYC auth, based on withdrawals and/or
 * KYC auth transfers.
 */
export async function runExchangeKycAuthTest(t: GlobalTestState) {
  // Set up test environment

  // FIXME: Reduced test environment without merchant suffices
  const {
    walletClient,
    bankClient,
    exchange,
    amlKeypair,
    exchangeBankAccount,
    exchangeApi,
    bank,
  } = await createKycTestkudosEnvironment(t, { adjustExchangeConfig });

  const merchantPayto = getTestHarnessPaytoForLabel("merchant-default");

  await bankClient.registerAccountExtended({
    name: "merchant-default",
    password: encodeCrock(getRandomBytes(32)),
    username: "merchant-default",
    payto_uri: merchantPayto,
  });

  const cryptoApi = createSyncCryptoApi();

  const wireGatewayApiClient = new TalerWireGatewayHttpClient(
    exchangeBankAccount.wireGatewayApiBaseUrl,
  );

  const merchantPair = await cryptoApi.createEddsaKeypair({});

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

  const kycPaytoHash = encodeCrock(hashNormalizedPaytoUri(merchantPayto));

  await wres.withdrawalFinishedCond;

  // Use the wallet to trigger KYC

  const depositResp = await walletClient.call(
    WalletApiOperation.CreateDepositGroup,
    {
      amount: "TESTKUDOS:5",
      depositPaytoUri: merchantPayto,
      testingFixedPriv: merchantPair.priv,
    },
  );

  // Wait until KYC got triggered.
  await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
    transactionId: depositResp.transactionId,
    txState: {
      major: TransactionMajorState.Pending,
      minor: TransactionMinorState.KycAuthRequired,
    },
  });

  await wireGatewayApiClient.addKycAuth({
    body: {
      account_pub: merchantPair.pub,
      amount: "TESTKUDOS:0.1",
      debit_account: merchantPayto,
    },
    auth: bank.getAdminAuth(),
  });

  await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
    transactionId: depositResp.transactionId,
    txState: {
      major: TransactionMajorState.Pending,
      minor: TransactionMinorState.KycRequired,
    },
  });

  {
    const sigResp = await cryptoApi.signWalletKycAuth({
      accountPriv: merchantPair.priv,
      accountPub: merchantPair.pub,
    });
    const checkResp1 = await exchangeApi.checkKycStatus({
      accountPub: merchantPair.pub,
      accountSig: sigResp.sig,
      paytoHash: kycPaytoHash,
    });

    console.log(j2s(checkResp1));
    // Time-sensitive which status will be returned.
    t.assertTrue(
      checkResp1.case === HttpStatusCode.Accepted ||
        checkResp1.case === HttpStatusCode.Ok,
    );
  }

  const reservePair2 = createEddsaKeyPair();
  const reservePair3 = createEddsaKeyPair();
  const reservePair4 = createEddsaKeyPair();

  {
    const sigResp = await cryptoApi.signWalletKycAuth({
      accountPriv: encodeCrock(reservePair2.eddsaPriv),
      accountPub: encodeCrock(reservePair2.eddsaPub),
    });
    const checkResp = await exchangeApi.checkKycStatus({
      accountPub: merchantPair.pub,
      accountSig: sigResp.sig,
      paytoHash: kycPaytoHash,
    });

    console.log(j2s(checkResp));

    t.assertDeepEqual(checkResp.case, HttpStatusCode.Forbidden);
  }

  await wireGatewayApiClient.addIncoming({
    body: {
      reserve_pub: encodeCrock(reservePair2.eddsaPub),
      amount: "TESTKUDOS:5",
      debit_account: merchantPayto,
    },
    auth: bank.getAdminAuth(),
  });

  await wireGatewayApiClient.addIncoming({
    body: {
      reserve_pub: encodeCrock(reservePair3.eddsaPub),
      amount: "TESTKUDOS:5",
      debit_account: merchantPayto,
    },
    auth: bank.getAdminAuth(),
  });

  await wireGatewayApiClient.addIncoming({
    body: {
      reserve_pub: encodeCrock(reservePair4.eddsaPub),
      amount: "TESTKUDOS:5",
      debit_account: getTestHarnessPaytoForLabel("bob"),
    },
    auth: bank.getAdminAuth(),
  });

  await exchange.runWirewatchOnce();

  // Even when no account pub is specified, the last reserve pub for
  // the account must work.

  {
    const sigResp = await cryptoApi.signWalletKycAuth({
      accountPriv: encodeCrock(reservePair3.eddsaPriv),
      accountPub: encodeCrock(reservePair3.eddsaPub),
    });
    const checkResp = await exchangeApi.testingCheckKycStatusNoPub({
      accountSig: sigResp.sig,
      paytoHash: kycPaytoHash,
    });

    console.log(j2s(checkResp));

    t.assertDeepEqual(checkResp.case, HttpStatusCode.Accepted);
  }

  // Now, kyc auth must work with explicit account key and both reserve pubs!

  {
    const sigResp = await cryptoApi.signWalletKycAuth({
      accountPriv: merchantPair.priv,
      accountPub: merchantPair.pub,
    });
    const checkResp = await exchangeApi.checkKycStatus({
      accountPub: merchantPair.pub,
      accountSig: sigResp.sig,
      paytoHash: kycPaytoHash,
    });

    console.log(j2s(checkResp));

    t.assertDeepEqual(checkResp.case, HttpStatusCode.Accepted);
  }

  {
    const sigResp = await cryptoApi.signWalletKycAuth({
      accountPriv: encodeCrock(reservePair3.eddsaPriv),
      accountPub: encodeCrock(reservePair3.eddsaPub),
    });
    const checkResp = await exchangeApi.checkKycStatus({
      accountPub: encodeCrock(reservePair3.eddsaPub),
      accountSig: sigResp.sig,
      paytoHash: kycPaytoHash,
    });

    console.log(j2s(checkResp));

    t.assertDeepEqual(checkResp.case, HttpStatusCode.Accepted);
  }

  {
    const sigResp = await cryptoApi.signWalletKycAuth({
      accountPriv: encodeCrock(reservePair2.eddsaPriv),
      accountPub: encodeCrock(reservePair2.eddsaPub),
    });
    const checkResp = await exchangeApi.checkKycStatus({
      accountPub: encodeCrock(reservePair2.eddsaPub),
      accountSig: sigResp.sig,
      paytoHash: kycPaytoHash,
    });

    console.log(j2s(checkResp));

    t.assertDeepEqual(checkResp.case, HttpStatusCode.Accepted);
  }

  await postAmlDecisionNoRules(t, {
    amlPriv: amlKeypair.priv,
    amlPub: amlKeypair.pub,
    exchangeBaseUrl: exchange.baseUrl,
    paytoHash: kycPaytoHash,
  });

  // Deposit should now work, even if it's *not* the latest reserve pub
  // being used, but the penultimate.
  const dep2Resp = await walletClient.call(
    WalletApiOperation.CreateDepositGroup,
    {
      amount: "TESTKUDOS:3",
      depositPaytoUri: merchantPayto,
      testingFixedPriv: encodeCrock(reservePair3.eddsaPriv),
    },
  );

  await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
    transactionId: dep2Resp.transactionId,
    txState: [
      {
        major: TransactionMajorState.Done,
      },
      {
        major: TransactionMajorState.Finalizing,
        minor: TransactionMinorState.Track,
      },
    ],
    timeout: { seconds: 10 },
  });

  await exchange.runAggregatorOnceWithTimetravel({
    timetravelMicroseconds: 10 * 60 * 1000 * 1000,
  });
  await exchange.runTransferOnceWithTimetravel({
    timetravelMicroseconds: 10 * 60 * 1000 * 1000,
  });

  await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
    transactionId: dep2Resp.transactionId,
    txState: [
      {
        major: TransactionMajorState.Done,
      },
    ],
  });
}

runExchangeKycAuthTest.suites = ["wallet"];
