/*
 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 { configureCommonKyc, createKycTestkudosEnvironment } from "../harness/environments.js";
import { GlobalTestState, harnessHttpLib, waitMs } from "../harness/harness.js";

const logger = new Logger("test-kyc-two-forms.ts");

export const AML_PROGRAM_NEXT_MEASURE_FORM: TalerKycAml.AmlProgramDefinition = {
  name: "TWO_FORMS",
  logic: async (input, config) => {
    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 TWO FORMS outcome", j2s(outcome));

    return outcome;
  },
  requiredAttributes: [],
  requiredInputs: [],
  requiredContext: [],
};

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

  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");

  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-M2", "check_name", "C2");
  config.setString("KYC-MEASURE-M2", "context", "{}");
  config.setString("KYC-MEASURE-M2", "program", "NONE");

  config.setString("KYC-MEASURE-M3", "check_name", "C3");
  config.setString("KYC-MEASURE-M3", "context", "{}");
  config.setString("KYC-MEASURE-M3", "program", "NONE");

  config.setString("KYC-MEASURE-M4", "check_name", "C4");
  config.setString("KYC-MEASURE-M4", "context", "{}");
  config.setString("KYC-MEASURE-M4", "program", "NONE");

  config.setString(
    "AML-PROGRAM-P1",
    "command",
    "taler-harness aml-program run-program --name TWO_FORMS",
  );
  config.setString("AML-PROGRAM-P1", "enabled", "true");
  config.setString("AML-PROGRAM-P1", "description", "remove all rules");
  config.setString("AML-PROGRAM-P1", "description_i18n", "{}");
  config.setString("AML-PROGRAM-P1", "fallback", "FREEZE");

  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");

  config.setString("KYC-CHECK-C2", "type", "FORM");
  config.setString("KYC-CHECK-C2", "form_name", "secondForm");
  config.setString("KYC-CHECK-C2", "description", "final check!");
  config.setString("KYC-CHECK-C2", "description_i18n", "{}");
  config.setString("KYC-CHECK-C2", "outputs", "FINAL");
  config.setString("KYC-CHECK-C2", "fallback", "FREEZE");

  config.setString("KYC-CHECK-C3", "type", "FORM");
  config.setString("KYC-CHECK-C3", "form_name", "thirdForm");
  config.setString(
    "KYC-CHECK-C3",
    "description",
    "this is check c3 (never used)",
  );
  config.setString("KYC-CHECK-C3", "description_i18n", "{}");
  config.setString("KYC-CHECK-C3", "fallback", "FREEZE");

  config.setString("KYC-CHECK-C4", "type", "FORM");
  config.setString("KYC-CHECK-C4", "form_name", "fourthForm");
  config.setString(
    "KYC-CHECK-C4",
    "description",
    "this is check c4 (never used)",
  );
  config.setString("KYC-CHECK-C4", "description_i18n", "{}");
  config.setString("KYC-CHECK-C4", "fallback", "FREEZE");
}

/**
 * Test setting a `new_measure` as the AML officer.
 */
export async function runKycTwoFormsTest(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");
    const infoResp = await harnessHttpLib.fetch(
      new URL(`kyc-upload/${latestFormId}`, exchange.baseUrl).href,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: { NAME: "who" },
      },
    );

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

  {
    logger.info(
      "step 5) Check KYC info again, should see the second form but this time is too fast",
    );

    ///////////////////////////////////////////
    // if the request is too early id result in garbage
    // can't be ignored because browser see this
    ///////////////////////////////////////////
    {
      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, "secondForm");
      }
    }
  }

  await waitMs(2000);
  {
    logger.info(
      "step 6) Check KYC info again after some time, should see the second form",
    );
    // doing a second request shouldn't fail
    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, "secondForm");
  }

  await waitMs(2000);

  {
    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, 1);
    t.assertDeepEqual(clientInfo?.requirements[0].form, "secondForm");
  }
}

runKycTwoFormsTest.suites = ["wallet"];
