/*
 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 {
  AccessToken,
  Configuration,
  encodeCrock,
  getRandomBytes,
  j2s,
  KycStatusLongPollingReason,
  Logger,
  parsePaytoUriOrThrow,
  succeedOrThrow,
  TalerMerchantInstanceHttpClient,
  TalerProtocolDuration,
  TalerWireGatewayHttpClient,
} from "@gnu-taler/taler-util";
import {
  configureCommonKyc,
  createKycTestkudosEnvironmentFull,
} from "../harness/environments.js";
import {
  BankService,
  ExchangeService,
  getTestHarnessPaytoForLabel,
  GlobalTestState,
  MerchantService,
} 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);
}

const logger = new Logger("test-merchant-kyc-auth-multi.ts");

async function doAccountKycAuth(
  t: GlobalTestState,
  args: {
    exchange: ExchangeService;
    merchant: MerchantService;
    bank: BankService;
    merchantInstId: string;
    merchantInstPaytoUri: string;
    merchantAccessToken: AccessToken;
    wireGatewayApi: TalerWireGatewayHttpClient;
  },
): Promise<void> {
  const {
    merchant,
    exchange,
    merchantInstId,
    merchantAccessToken,
    merchantInstPaytoUri,
    bank,
    wireGatewayApi,
  } = args;
  const merchantClient = new TalerMerchantInstanceHttpClient(
    merchant.makeInstanceBaseUrl(merchantInstId),
  );
  {
    const kycRes1 = succeedOrThrow(
      await merchantClient.getCurrentInstanceKycStatus(merchantAccessToken, {}),
    );
    console.log(`kyc res: ${j2s(kycRes1)}`);
    const myRow = kycRes1.kyc_data.find(
      (x) => x.exchange_url === exchange.baseUrl,
    );
    t.assertTrue(
      myRow?.payto_kycauths != null && myRow.payto_kycauths.length == 1,
    );
    const authTxPayto = parsePaytoUriOrThrow(myRow.payto_kycauths[0]);
    const authTxMessage = authTxPayto?.params["message"];
    t.assertTrue(typeof authTxMessage === "string");
    t.assertTrue(authTxMessage.startsWith("KYC:"));
    const accountPub = authTxMessage.substring(4);
    logger.info(`merchant account pub: ${accountPub}`);
    await wireGatewayApi.addKycAuth({
      auth: bank.getAdminAuth(),
      body: {
        amount: "TESTKUDOS:0.1",
        debit_account: merchantInstPaytoUri,
        account_pub: accountPub,
      },
    });
  }

  // Wait for auth transfer to be registered by the exchange
  {
    const kycStatus = await merchantClient.getCurrentInstanceKycStatus(
      merchantAccessToken,
      {
        reason: KycStatusLongPollingReason.AUTH_TRANSFER,
        timeout: 30000,
      },
    );
    logger.info(`kyc status after transfer: ${j2s(kycStatus)}`);
    t.assertDeepEqual(kycStatus.case, "ok");
    const myRow = kycStatus.body.kyc_data.find(
      (x) =>
        x.exchange_url === exchange.baseUrl &&
        x.payto_uri === merchantInstPaytoUri,
    );
    t.assertTrue(myRow != null);
    t.assertDeepEqual(myRow.status, "ready");
    t.assertTrue(typeof myRow.access_token === "string");
  }
}

/**
 * Test for multiple merchant instances using the same
 * bank account (with multiple public keys and thus KYC auth transfers).
 */
export async function runMerchantKycAuthMultiTest(t: GlobalTestState) {
  // Set up test environment

  const {
    bankClient,
    merchant,
    bank,
    exchange,
    merchantAdminAccessToken,
    wireGatewayApi,
  } = await createKycTestkudosEnvironmentFull(t, { adjustExchangeConfig });

  {
    const merchantInstId = "minst1";
    const merchantInstPaytoUri = getTestHarnessPaytoForLabel(merchantInstId);

    await bankClient.registerAccountExtended({
      name: merchantInstId,
      password: encodeCrock(getRandomBytes(32)),
      username: merchantInstId,
      payto_uri: merchantInstPaytoUri,
    });

    const m1Res = await merchant.addInstanceWithWireAccount(
      {
        id: merchantInstId,
        name: merchantInstId,
        paytoUris: [merchantInstPaytoUri],
        defaultWireTransferDelay: TalerProtocolDuration.fromSpec({
          minutes: 1,
        }),
      },
      { adminAccessToken: merchantAdminAccessToken },
    );

    const merchantAccessToken = m1Res.accessToken;

    await merchant.runExchangekeyupdateOnce();
    await merchant.runKyccheckOnce();

    await doAccountKycAuth(t, {
      exchange,
      merchant,
      bank,
      merchantInstId,
      merchantAccessToken,
      merchantInstPaytoUri,
      wireGatewayApi,
    });

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

    const accounts1 = succeedOrThrow(
      await merchantClient.listBankAccounts(m1Res.accessToken),
    );
    t.assertDeepEqual(accounts1.accounts.length, 1);
    await merchantClient.deleteBankAccount(
      merchantAccessToken,
      accounts1.accounts[0].h_wire,
    );
    const accounts2 = succeedOrThrow(
      await merchantClient.listBankAccounts(m1Res.accessToken),
    );
    console.log(`accounts after deletion: ${j2s(accounts2)}`);

    // Now re-add same account
    succeedOrThrow(
      await merchantClient.addBankAccount(m1Res.accessToken, {
        payto_uri: merchantInstPaytoUri,
      }),
    );

    const accounts3 = succeedOrThrow(
      await merchantClient.listBankAccounts(m1Res.accessToken),
    );
    console.log(`accounts after re-adding: ${j2s(accounts3)}`);

    // Now we should now have to do KYC auth again, same instance,
    // account was just re-added.
    {
      const kycStatus = await merchantClient.getCurrentInstanceKycStatus(
        merchantAccessToken,
        {
          reason: KycStatusLongPollingReason.AUTH_TRANSFER,
          timeout: 30000,
        },
      );
      logger.info(`kyc status after transfer: ${j2s(kycStatus)}`);
      t.assertDeepEqual(kycStatus.case, "ok");
      const myRow = kycStatus.body.kyc_data.find(
        (x) =>
          x.exchange_url === exchange.baseUrl &&
          x.payto_uri === merchantInstPaytoUri,
      );
      t.assertTrue(myRow != null);
      t.assertDeepEqual(myRow.status, "ready");
      t.assertTrue(typeof myRow.access_token === "string");
    }
  }

  t.logStep("fist-account-ready");

  {
    const merchantInstId = "minst2";
    const merchantInstPaytoUri = getTestHarnessPaytoForLabel(merchantInstId);

    await bankClient.registerAccountExtended({
      name: merchantInstId,
      password: encodeCrock(getRandomBytes(32)),
      username: merchantInstId,
      payto_uri: merchantInstPaytoUri,
    });

    const m2Res = await merchant.addInstanceWithWireAccount(
      {
        id: merchantInstId,
        name: merchantInstId,
        paytoUris: [merchantInstPaytoUri],
        defaultWireTransferDelay: TalerProtocolDuration.fromSpec({
          minutes: 1,
        }),
      },
      { adminAccessToken: merchantAdminAccessToken },
    );

    await merchant.runExchangekeyupdateOnce();
    await merchant.runKyccheckOnce();

    await doAccountKycAuth(t, {
      exchange,
      merchant,
      bank,
      merchantInstId,
      merchantAccessToken: m2Res.accessToken,
      merchantInstPaytoUri,
      wireGatewayApi,
    });
  }

  t.logStep("second-account-ready");
}

runMerchantKycAuthMultiTest.suites = ["wallet"];
