/*
 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,
  encodeCrock,
  hashNormalizedPaytoUri,
  j2s,
  Logger,
  MerchantAccountKycRedirectsResponse,
  MerchantAccountKycStatus,
  PaytoString,
  succeedOrThrow,
} from "@gnu-taler/taler-util";
import {
  configureCommonKyc,
  createKycTestkudosEnvironment,
  postAmlDecisionNoRules,
} from "../harness/environments.js";
import { delayMs, GlobalTestState } from "../harness/harness.js";

const logger = new Logger(`test-kyc-merchant-deposit-rewrite.ts`);

function adjustExchangeConfig(config: Configuration) {
  configureCommonKyc(config);

  config.setString("KYC-RULE-R1", "operation_type", "deposit");
  config.setString("KYC-RULE-R1", "enabled", "yes");
  config.setString("KYC-RULE-R1", "exposed", "yes");
  config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
  config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:0");
  config.setString("KYC-RULE-R1", "timeframe", "1d");
  config.setString("KYC-RULE-R1", "next_measures", "M1");

  config.setString("KYC-MEASURE-M1", "check_name", "C1");
  config.setString("KYC-MEASURE-M1", "context", "{}");
  config.setString("KYC-MEASURE-M1", "program", "P1");

  config.setString("KYC-MEASURE-FM", "check_name", "SKIP");
  config.setString("KYC-MEASURE-FM", "context", "{}");
  config.setString("KYC-MEASURE-FM", "program", "P1");

  config.setString("AML-PROGRAM-P1", "command", "/bin/true");
  config.setString("AML-PROGRAM-P1", "enabled", "true");
  config.setString("AML-PROGRAM-P1", "description", "this does nothing");
  config.setString("AML-PROGRAM-P1", "fallback", "FREEZE");

  config.setString("KYC-CHECK-C1", "type", "INFO");
  config.setString("KYC-CHECK-C1", "description", "my check!");
  config.setString("KYC-CHECK-C1", "fallback", "FREEZE");
}

function adjustMerchantConfig(config: Configuration) {
  config.setString("merchant-kyccheck", "aml_freq", "2s");
  config.setString("merchant-kyccheck", "aml_low_freq", "5s");
}

/**
 * Retry until a maximum of retries / timeout has passed,
 * or we get a non-null result.
 */
async function retryUntilNonNull<T>(
  fx: () => Promise<T | undefined>,
  maxRetry: number = 15,
  waitMs: number = 1000,
): Promise<T> {
  let i = 0;
  while (true) {
    const res = await fx();
    if (res != null) {
      return res;
    }
    logger.info(`retrying since result was null-ish: ${j2s({ time: i })}`);
    await delayMs(waitMs);
    i++;
    if (i > maxRetry) {
      throw Error("Too many retries");
    }
  }
}

/**
 * Alternate version of the kyc-merchant-deposit test.
 *
 * This test invokes kyccheck in test mode instead of creating an order.
 */
export async function runKycMerchantDepositRewriteTest(t: GlobalTestState) {
  // Set up test environment

  const {
    bank,
    amlKeypair,
    bankApi,
    merchantApi,
    wireGatewayApi,
    merchant,
    merchantAdminAccessToken,
    exchangeApi,
  } = await createKycTestkudosEnvironment(t, {
    adjustExchangeConfig,
    adjustMerchantConfig,
  });

  let exchangeWireTarget: string | undefined;
  let merchantBankAccount: PaytoString | undefined;
  {
    const kycStatus = await retryUntilNonNull(async () => {
      const res = succeedOrThrow<MerchantAccountKycRedirectsResponse | void>(
        await merchantApi.getCurrentInstanceKycStatus(merchantAdminAccessToken),
      );
      if (
        res &&
        res.kyc_data[0].status !== MerchantAccountKycStatus.EXCHANGE_UNREACHABLE
      ) {
        return res;
      }
      return undefined;
    });

    logger.info(`mechant kyc status: ${j2s(kycStatus)}`);
    t.assertTrue(!!kycStatus);

    t.assertTrue(
      kycStatus.kyc_data.length > 0,
      "no bank account reported by the exchange in the kyc query",
    );
    const firstKycStatus = kycStatus.kyc_data[0];

    t.assertDeepEqual(
      firstKycStatus.status,
      MerchantAccountKycStatus.KYC_WIRE_REQUIRED,
    );

    t.assertDeepEqual(firstKycStatus.exchange_http_status, 404);

    t.assertTrue(
      (firstKycStatus.limits?.length ?? 0) > 0,
      "kyc status should contain non-empty limits",
    );

    t.assertTrue(
      firstKycStatus.payto_kycauths !== undefined,
      "exchange should give information on how to make the auth wire transfer",
    );
    t.assertTrue(
      firstKycStatus.payto_kycauths.length > 0,
      "exchange should at least one bank account to wire",
    );
    merchantBankAccount = firstKycStatus.payto_uri;
    exchangeWireTarget = firstKycStatus.payto_kycauths[0];
  }

  logger.info("We need to wire some money to ", exchangeWireTarget);

  succeedOrThrow(
    await bankApi.createAccount(merchantAdminAccessToken, {
      name: "merchant-default",
      password: "merchant-default",
      username: "merchant-default",
      // this bank user needs to have the same payto that the exchange is asking from
      payto_uri: merchantBankAccount,
    }),
  );
  logger.info("Bank account created");

  const info = succeedOrThrow(
    await merchantApi.getCurrentInstanceDetails(merchantAdminAccessToken),
  );

  succeedOrThrow(
    await wireGatewayApi.addKycAuth({
      auth: bank.getAdminAuth(),
      body: {
        account_pub: info.merchant_pub,
        amount: "TESTKUDOS:0.1",
        debit_account: merchantBankAccount,
      },
    }),
  );
  logger.info("Money wired");

  let merchantAmlAccount;
  {
    const kycStatus = await retryUntilNonNull(async () => {
      await merchant.runKyccheckOnce();
      const x = succeedOrThrow<MerchantAccountKycRedirectsResponse | void>(
        await merchantApi.getCurrentInstanceKycStatus(merchantAdminAccessToken),
      );
      if (!!x && !x.kyc_data[0].payto_kycauths) {
        return x;
      }
      return undefined;
    });
    logger.info(`kyc resp 2: ${j2s(kycStatus)}`);

    t.assertTrue(!!kycStatus);

    t.assertTrue(
      kycStatus.kyc_data.length > 0,
      "no bank account reported by the exchange in the kyc query",
    );
    const firstBankAccount = kycStatus.kyc_data[0];
    t.assertDeepEqual(
      firstBankAccount.status,
      MerchantAccountKycStatus.KYC_REQUIRED,
    );

    t.assertDeepEqual(firstBankAccount.exchange_http_status, 200);

    t.assertTrue(
      (firstBankAccount.limits?.length ?? 0) > 0,
      "kyc status should contain non-empty limits",
    );

    merchantAmlAccount = encodeCrock(
      hashNormalizedPaytoUri(firstBankAccount.payto_uri),
    );
  }
  logger.info(
    "the aml officer is going to activate the account ",
    merchantAmlAccount,
  );

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

  {
    const kycStatus = await retryUntilNonNull(async () => {
      // Required, since the merchant only checks infrequently.
      await merchant.runKyccheckOnce();
      // Now we can check the status
      const x = succeedOrThrow<MerchantAccountKycRedirectsResponse | void>(
        await merchantApi.getCurrentInstanceKycStatus(merchantAdminAccessToken),
      );
      if ((x?.kyc_data[0].limits?.length ?? 0) === 0) {
        return x;
      }
      return undefined;
    });
    t.assertTrue(!!kycStatus);
    logger.info(`kyc resp 3: ${j2s(kycStatus)}`);
  }
}

runKycMerchantDepositRewriteTest.suites = ["wallet", "merchant", "kyc"];
