/*
 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 {
  AmountString,
  bufferFromAmount,
  buildSigPS,
  codecForAccountKycStatus,
  codecForKycProcessClientInformation,
  codecForLegitimizationNeededResponse,
  codecOptional,
  Configuration,
  createNewWalletKycAccount,
  eddsaSign,
  encodeCrock,
  j2s,
  Logger,
  TalerKycAml,
  TalerSignaturePurpose,
  WalletKycRequest,
} from "@gnu-taler/taler-util";
import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { execSync } from "node:child_process";
import {
  configureCommonKyc,
  createKycTestkudosEnvironment,
} from "../harness/environments.js";
import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";

const logger = new Logger(`test-kyc-merchant-fail-recover.ts`);

export const AML_PROGRAM_FAIL_RECOVER: TalerKycAml.AmlProgramDefinition = {
  name: "FAIL_RECOVER",
  logic: async (input, config) => {
    logger.info("INPUT", input);
    if (input?.attributes?.NAME === "trow-error") {
      throw Error("i was asked to fail");
    }
    if (input?.attributes?.NAME === "exit-fail") {
      process.exit(1);
    }
    if (input?.attributes?.NAME === "child-fail") {
      execSync("something-that-doesnt-exists make-this-fail");
    }

    const outcome: TalerKycAml.AmlOutcome = {
      to_investigate: false,
      properties: {},
      events: [],
      new_rules: {
        expiration_time: { t_s: 1 },
        rules: [],
        successor_measure: "M2",
        custom_measures: {},
      },
    };
    logger.info("aml program FAIL AND RECOVER outcome", j2s(outcome));

    return outcome;
  },
  requiredAttributes: ["NAME"],
  requiredInputs: ["attributes"],
  requiredContext: [],
};

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

  // trigger based on balance
  config.setString("KYC-RULE-R1", "operation_type", "balance");
  config.setString("KYC-RULE-R1", "enabled", "yes");
  config.setString("KYC-RULE-R1", "exposed", "yes");
  config.setString("KYC-RULE-R1", "is_and_combinator", "no");
  config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
  config.setString("KYC-RULE-R1", "timeframe", "forever");
  config.setString("KYC-RULE-R1", "next_measures", "M1");

  // normal measure: form and program
  config.setString("KYC-MEASURE-M1", "check_name", "C1");
  config.setString("KYC-MEASURE-M1", "context", "{}");
  config.setString("KYC-MEASURE-M1", "program", "P1");

  // a program that will fail based on the form input
  config.setString(
    "AML-PROGRAM-P1",
    "command",
    "taler-harness aml-program run-program --name FAIL_RECOVER",
  );
  config.setString("AML-PROGRAM-P1", "enabled", "true");
  config.setString(
    "AML-PROGRAM-P1",
    "description",
    "just fail based on the name",
  );
  config.setString("AML-PROGRAM-P1", "description_i18n", "{}");
  config.setString("AML-PROGRAM-P1", "fallback", "FREEZE");

  // normal form
  config.setString("KYC-CHECK-C1", "type", "FORM");
  config.setString("KYC-CHECK-C1", "form_name", "firstForm");
  config.setString("KYC-CHECK-C1", "description", "starting check!");
  config.setString("KYC-CHECK-C1", "description_i18n", "{}");
  config.setString("KYC-CHECK-C1", "outputs", "NAME");
  config.setString("KYC-CHECK-C1", "fallback", "FREEZE");
}

export async function runKycFailRecoverSimpleTest(t: GlobalTestState) {
  // Set up test environment

  const { exchange, amlKeypair } = await createKycTestkudosEnvironment(t, {
    adjustExchangeConfig,
    onWalletNotification: () => {},
  });

  // Withdraw digital cash into the wallet.
  let kycPaytoHash: string;
  let accessToken: string;
  let latestFormId: string;

  const account = await createNewWalletKycAccount(new Uint8Array());
  logger.info("step 1) Check balance to trigger AML");
  {
    const balance: AmountString = "TESTKUDOS:20";
    const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP)
      .put(bufferFromAmount(balance))
      .build();
    const body: WalletKycRequest = {
      balance,
      reserve_pub: account.id,
      reserve_sig: encodeCrock(eddsaSign(sigBlob, account.signingKey)),
    };
    const infoResp = await harnessHttpLib.fetch(
      new URL(`kyc-wallet`, exchange.baseUrl).href,
      {
        method: "POST",
        body,
      },
    );

    t.assertDeepEqual(infoResp.status, 451);
    const clientInfo = await readResponseJsonOrThrow(
      infoResp,
      codecOptional(codecForLegitimizationNeededResponse()),
    );

    t.assertTrue(clientInfo?.h_payto !== undefined);
    kycPaytoHash = clientInfo?.h_payto;
  }

  logger.info("step 2) Get account access token");
  {
    const sigBlob = buildSigPS(TalerSignaturePurpose.KYC_AUTH).build();

    const infoResp = await harnessHttpLib.fetch(
      new URL(`kyc-check/${kycPaytoHash}`, exchange.baseUrl).href,
      {
        headers: {
          "Account-Owner-Signature": encodeCrock(
            eddsaSign(sigBlob, account.signingKey),
          ),
        },
      },
    );

    t.assertDeepEqual(infoResp.status, 202);
    const clientInfo = await readResponseJsonOrThrow(
      infoResp,
      codecOptional(codecForAccountKycStatus()),
    );
    t.assertTrue(clientInfo?.access_token !== undefined);
    accessToken = clientInfo?.access_token;
  }

  logger.info("step 3) Check KYC info, should be waiting for the first form");
  {
    const infoResp = await harnessHttpLib.fetch(
      new URL(`kyc-info/${accessToken}?timeout_ms=1000`, exchange.baseUrl).href,
    );
    const clientInfo = await readResponseJsonOrThrow(
      infoResp,
      codecOptional(codecForKycProcessClientInformation()),
    );

    console.log(j2s(clientInfo));
    t.assertDeepEqual(infoResp.status, 200);
    t.assertDeepEqual(clientInfo?.requirements.length, 1);
    t.assertDeepEqual(clientInfo?.requirements[0].form, "firstForm");
    t.assertTrue(!!clientInfo?.requirements[0].id);
    latestFormId = clientInfo?.requirements[0].id;
  }

  logger.info("step 4) Complete form expecting to fail");
  {
    const infoResp = await harnessHttpLib.fetch(
      new URL(`kyc-upload/${latestFormId}`, exchange.baseUrl).href,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: { NAME: "child-fail" },
      },
    );

    t.assertDeepEqual(infoResp.status, 500);
  }

  logger.info("step 5) Complete form expecting but this time it should work");
  {
    const infoResp = await harnessHttpLib.fetch(
      new URL(`kyc-upload/${latestFormId}`, exchange.baseUrl).href,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: { NAME: "just-kidding" },
      },
    );

    logger.info(`kyc-upload response: ${j2s(await infoResp.json())}`);

    t.assertDeepEqual(infoResp.status, 409);
  }

  {
    logger.info(
      "step 6) Check KYC info again after some time, here the exchange fails",
    );
    const infoResp = await harnessHttpLib.fetch(
      new URL(`kyc-info/${accessToken}?timeout_ms=1000`, exchange.baseUrl).href,
    );

    const clientInfo = await readResponseJsonOrThrow(
      infoResp,
      codecOptional(codecForKycProcessClientInformation()),
    );

    console.log(j2s(clientInfo));
    t.assertDeepEqual(infoResp.status, 200);
    t.assertDeepEqual(clientInfo?.requirements.length, 0);
  }
}

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