/*
 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 {
  codecForAny,
  codecForKycProcessClientInformation,
  codecOptional,
  Configuration,
  decodeCrock,
  encodeCrock,
  j2s,
  signAmlQuery,
  TalerKycAml,
  TalerProtocolTimestamp,
  TransactionIdStr,
  TransactionMajorState,
  TransactionMinorState,
} from "@gnu-taler/taler-util";
import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
  configureCommonKyc,
  createKycTestkudosEnvironment,
  postAmlDecision,
  withdrawViaBankV3,
} from "../harness/environments.js";
import { GlobalTestState, harnessHttpLib, waitMs } from "../harness/harness.js";

export const AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT: TalerKycAml.AmlProgramDefinition =
  {
    name: "from-attr-to-context",
    logic: async (_input, config) => {
      // Artificially delay the AML program.
      await waitMs(500);
      const outcome: TalerKycAml.AmlOutcome = {
        to_investigate: false,
        // pushing to info into properties for testing purposes
        properties: {
          "this comes": "from the program",
          input: _input as any,
          config,
        },
        events: [],
        new_rules: {
          expiration_time: TalerProtocolTimestamp.zero(),
          rules: [],
          successor_measure: "ask_more_info",
          custom_measures: {
            ask_more_info: {
              context: {
                // this is the context info that the KYC-SPA will see
                WAT: "REALLY?",
              },
              check_name: "C2",
              prog_name: "P2",
            },
          },
        },
      };
      return outcome;
    },
    requiredAttributes: [],
    requiredInputs: [],
    requiredContext: [],
  };

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

  config.setString("KYC-RULE-R1", "operation_type", "withdraw");
  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", "1d");
  config.setString("KYC-RULE-R1", "next_measures", "M2");

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

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

  config.setString(
    "AML-PROGRAM-P1",
    "command",
    "taler-harness aml-program run-program --name from-attr-to-context",
  );
  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("AML-PROGRAM-P2", "command", "/bin/true");
  config.setString("AML-PROGRAM-P2", "enabled", "true");
  config.setString("AML-PROGRAM-P2", "description", "does nothing");
  config.setString("AML-PROGRAM-P2", "description_i18n", "{}");
  config.setString("AML-PROGRAM-P2", "fallback", "FREEZE");

  config.setString("KYC-CHECK-C1", "type", "FORM");
  config.setString("KYC-CHECK-C1", "form_name", "myform");
  config.setString("KYC-CHECK-C1", "description", "my check!");
  config.setString("KYC-CHECK-C1", "description_i18n", "{}");
  config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate");
  config.setString("KYC-CHECK-C1", "fallback", "FREEZE");

  config.setString("KYC-CHECK-C2", "type", "FORM");
  config.setString("KYC-CHECK-C2", "form_name", "dynamicform");
  config.setString("KYC-CHECK-C2", "description", "my check info!");
  config.setString("KYC-CHECK-C2", "description_i18n", "{}");
  config.setString("KYC-CHECK-C1", "outputs", "what_the_officer_asked");
  config.setString("KYC-CHECK-C2", "fallback", "FREEZE");

  config.setString("KYC-CHECK-C3", "type", "INFO");
  config.setString("KYC-CHECK-C3", "description", "this is info c3");
  config.setString("KYC-CHECK-C3", "description_i18n", "{}");
  config.setString("KYC-CHECK-C3", "fallback", "FREEZE");
}

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

  const { walletClient, bankClient, exchange, amlKeypair } =
    await createKycTestkudosEnvironment(t, { adjustExchangeConfig });

  // Withdraw digital cash into the wallet.
  let kycPaytoHash: string | undefined;
  let accessToken: string | undefined;
  let firstTransaction: string | undefined;

  {
    // step 1) Withdraw to trigger AML
    const wres = await withdrawViaBankV3(t, {
      amount: "TESTKUDOS:20",
      bankClient,
      exchange,
      walletClient,
    });

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

    const txDetails = await walletClient.call(
      WalletApiOperation.GetTransactionById,
      {
        transactionId: wres.transactionId,
      },
    );

    accessToken = txDetails.kycAccessToken;
    kycPaytoHash = txDetails.kycPaytoHash;
    firstTransaction = wres.transactionId;
  }

  t.assertTrue(!!accessToken);

  {
    // step 2) Check KYC info
    const infoResp = await harnessHttpLib.fetch(
      new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href,
    );

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

    console.log(j2s(clientInfo));
    t.assertDeepEqual(infoResp.status, 200);
  }

  const sig = signAmlQuery(decodeCrock(amlKeypair.priv));
  {
    // step 3) Apply Measure 3 with SKIP check
    const decisionsResp = await harnessHttpLib.fetch(
      new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href,
      {
        headers: {
          "Taler-AML-Officer-Signature": encodeCrock(sig),
        },
      },
    );

    console.log(decisionsResp.status);
    t.assertDeepEqual(decisionsResp.status, 204);

    t.assertTrue(!!kycPaytoHash);

    await postAmlDecision(t, {
      amlPriv: amlKeypair.priv,
      amlPub: amlKeypair.pub,
      exchangeBaseUrl: exchange.baseUrl,
      paytoHash: kycPaytoHash,
      newMeasures: "M3",
      properties: {
        form: { name: "string" },
      },
      newRules: {
        expiration_time: TalerProtocolTimestamp.now(),
        custom_measures: {},
        rules: [
          // No rules!
        ],
      },
    });
  }

  {
    // step 4) Check KYC info, it should have the result
    // of running program p1
    const decisionsResp = await harnessHttpLib.fetch(
      new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href,
      {
        headers: {
          "Taler-AML-Officer-Signature": encodeCrock(sig),
        },
      },
    );

    const decisions = await readResponseJsonOrThrow(
      decisionsResp,
      codecForAny(),
    );
    console.log(j2s(decisions));

    t.assertDeepEqual(decisionsResp.status, 200);
  }

  // Make sure that there can be another decision
  await waitMs(2000);

  // Wait for the KYC program to run
  while (true) {
    const infoResp = await harnessHttpLib.fetch(
      new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href,
    );

    console.log(`kyc-info status: ${infoResp.status}`);
    if (infoResp.status == 202) {
      await waitMs(1000);
      continue;
    }
    // KYC program still busy.
    // In the future, this should long-poll.
    if (infoResp.status == 204) {
      await waitMs(1000);
      continue;
    }

    const respJson = await infoResp.json();
    console.log(j2s(respJson));

    t.assertDeepEqual(infoResp.status, 200);

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

    console.log(j2s(clientInfo));

    // Finally here we must see the officer defined form
    t.assertDeepEqual(clientInfo?.requirements[0].context, {
      // this is fixed by the aml program
      WAT: "REALLY?",
    });

    break;
  }
}

runKycSkipExpirationTest.suites = ["wallet"];
