/*
 This file is part of GNU Taler
 (C) 2020 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 {
  Logger,
  NotificationType,
  TransactionMajorState,
  TransactionMinorState,
  TransactionType,
  codecForKycProcessClientInformation,
  j2s,
} from "@gnu-taler/taler-util";
import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as http from "node:http";
import {
  configureCommonKyc,
  createKycTestkudosEnvironment,
} from "../harness/environments.js";
import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";

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

interface TestfakeKycService {
  stop: () => void;
}

function splitInTwoAt(s: string, separator: string): [string, string] {
  const idx = s.indexOf(separator);
  if (idx === -1) {
    return [s, ""];
  }
  return [s.slice(0, idx), s.slice(idx + 1)];
}

/**
 * Testfake for the kyc service that the exchange talks to.
 */
async function runTestfakeKycService(): Promise<TestfakeKycService> {
  const server = http.createServer((req, res) => {
    const requestUrl = req.url!;
    logger.info(`kyc: got ${req.method} request, ${requestUrl}`);

    const [path, query] = splitInTwoAt(requestUrl, "?");

    const qp = new URLSearchParams(query);

    if (path === "/oauth/v2/login") {
      // Usually this would render some HTML page for the user to log in,
      // but we return JSON here.
      const redirUriUnparsed = qp.get("redirect_uri");
      if (!redirUriUnparsed) {
        throw Error("missing redirect_url");
      }
      const state = qp.get("state");
      if (!state) {
        throw Error("missing state");
      }
      const redirUri = new URL(redirUriUnparsed);
      redirUri.searchParams.set("code", "code_is_ok");
      redirUri.searchParams.set("state", state);
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(
        JSON.stringify({
          redirect_uri: redirUri.href,
        }),
      );
    } else if (path === "/oauth/v2/token") {
      let reqBody = "";
      req.on("data", (x) => {
        reqBody += x;
      });

      req.on("end", () => {
        logger.info("login request body:", reqBody);

        res.writeHead(200, { "Content-Type": "application/json" });
        // Normally, the access_token would also include which user we're trying
        // to get info about, but we (for now) skip it in this test.
        res.end(
          JSON.stringify({
            access_token: "exchange_access_token",
            token_type: "Bearer",
          }),
        );
      });
    } else if (path === "/oauth/v2/info") {
      logger.info("authorization header:", req.headers.authorization);
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(
        JSON.stringify({
          status: "success",
          data: {
            id: "Foobar",
            full_name: "Alice",
            last_name: "Abc",
            birthdate: "2000-01-01",
          },
        }),
      );
    } else {
      res.writeHead(400, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ code: 1, message: "bad request" }));
    }
  });
  await new Promise<void>((resolve, reject) => {
    server.listen(6666, () => resolve());
  });
  return {
    stop() {
      server.close();
    },
  };
}

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

  const { walletClient, bankClient, exchange, merchant } =
    await createKycTestkudosEnvironment(t, {
      adjustExchangeConfig(config) {
        config.setString("exchange", "enable_kyc", "yes");

        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", "yes");
        config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
        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-CHECK-C1", "type", "LINK");
        config.setString("KYC-CHECK-C1", "provider_id", "MYPROV");
        config.setString("KYC-CHECK-C1", "description", "my check!");
        config.setString("KYC-CHECK-C1", "description_i18n", "{}");
        config.setString("KYC-CHECK-C1", "outputs", "FULL_NAME DATE_OF_BIRTH");
        config.setString("KYC-CHECK-C1", "fallback", "FREEZE");

        config.setString(
          "AML-PROGRAM-P1",
          "command",
          "taler-exchange-helper-measure-test-form",
        );
        config.setString("AML-PROGRAM-P1", "enabled", "true");
        config.setString(
          "AML-PROGRAM-P1",
          "description",
          "test for FULL_NAME and DATE_OF_BIRTH",
        );
        config.setString("AML-PROGRAM-P1", "description_i18n", "{}");
        config.setString("AML-PROGRAM-P1", "fallback", "FREEZE");

        const myprov = "KYC-PROVIDER-MYPROV";
        config.setString(myprov, "logic", "oauth2");
        config.setString(
          myprov,
          "converter",
          "taler-exchange-kyc-oauth2-test-converter.sh",
        );
        config.setString(myprov, "kyc_oauth2_validity", "forever");
        config.setString(
          myprov,
          "kyc_oauth2_token_url",
          "http://localhost:6666/oauth/v2/token",
        );
        config.setString(
          myprov,
          "kyc_oauth2_authorize_url",
          "http://localhost:6666/oauth/v2/login",
        );
        config.setString(
          myprov,
          "kyc_oauth2_info_url",
          "http://localhost:6666/oauth/v2/info",
        );
        config.setString(
          myprov,
          "kyc_oauth2_converter_helper",
          "taler-exchange-kyc-oauth2-test-converter.sh",
        );
        config.setString(myprov, "kyc_oauth2_client_id", "taler-exchange");
        config.setString(myprov, "kyc_oauth2_client_secret", "exchange-secret");
        config.setString(myprov, "kyc_oauth2_post_url", "https://taler.net");

        config.setString(
          "kyc-legitimization-withdraw1",
          "operation_type",
          "withdraw",
        );
      },
    });

  const kycServer = await runTestfakeKycService();

  // Withdraw digital cash into the wallet.

  const amount = "TESTKUDOS:20";
  const user = await bankClient.createRandomBankUser();
  bankClient.setAuth({
    username: user.username,
    password: user.password,
  });

  const wop = await bankClient.createWithdrawalOperation(user.username, amount);

  // Hand it to the wallet

  await walletClient.client.call(
    WalletApiOperation.GetWithdrawalDetailsForUri,
    {
      talerWithdrawUri: wop.taler_withdraw_uri,
    },
  );

  // Withdraw

  const acceptResp = await walletClient.client.call(
    WalletApiOperation.AcceptBankIntegratedWithdrawal,
    {
      exchangeBaseUrl: exchange.baseUrl,
      talerWithdrawUri: wop.taler_withdraw_uri,
    },
  );

  const withdrawalTxId = acceptResp.transactionId;

  // Confirm it

  await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
    transactionId: withdrawalTxId,
    txState: {
      major: TransactionMajorState.Pending,
      minor: TransactionMinorState.BankConfirmTransfer,
    },
  });

  await bankClient.confirmWithdrawalOperation(user.username, {
    withdrawalOperationId: wop.withdrawal_id,
  });

  const kycNotificationCond = walletClient.waitForNotificationCond((x) => {
    if (
      x.type === NotificationType.TransactionStateTransition &&
      x.transactionId === withdrawalTxId &&
      x.newTxState.major === TransactionMajorState.Pending &&
      x.newTxState.minor === TransactionMinorState.KycRequired
    ) {
      return x;
    }
    return false;
  });

  const withdrawalDoneCond = walletClient.waitForNotificationCond(
    (x) =>
      x.type === NotificationType.TransactionStateTransition &&
      x.transactionId === withdrawalTxId &&
      x.newTxState.major === TransactionMajorState.Done,
  );

  const kycNotif = await kycNotificationCond;

  logger.info("got kyc notification:", j2s(kycNotif));

  const txState = await walletClient.client.call(
    WalletApiOperation.GetTransactionById,
    {
      transactionId: withdrawalTxId,
    },
  );

  t.assertDeepEqual(txState.type, TransactionType.Withdrawal);
  const paytoHash = txState.kycPaytoHash;

  t.assertTrue(!!txState.kycUrl);
  t.assertTrue(!!paytoHash);

  // We now simulate the user interacting with the KYC service,
  // which would usually done in the browser.

  const accessToken = txState.kycAccessToken;
  t.assertTrue(!!accessToken);

  /**
   * TODO: we should check this in another way that doesn't make the test
   * to normally take 3 seconds on happy path
   */
  const unexpectedNotification =
    await walletClient.waitForNotificationCondOrTimeout((x) => {
      if (
        x.type === NotificationType.TransactionStateTransition &&
        x.transactionId === withdrawalTxId &&
        x.newTxState.major === TransactionMajorState.Pending &&
        x.newTxState.minor === TransactionMinorState.WithdrawCoins
      ) {
        return x;
      }
      return false;
    }, 3000);

  if (unexpectedNotification) {
    throw Error(`unexpected notification ${j2s(unexpectedNotification)}`);
  }

  const infoResp = await harnessHttpLib.fetch(
    new URL(`kyc-info/${txState.kycAccessToken}`, exchange.baseUrl).href,
  );

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

  console.log(j2s(clientInfo));

  const kycId = clientInfo.requirements.find((x) => x.id != null)?.id;
  t.assertTrue(!!kycId);

  const startResp = await harnessHttpLib.fetch(
    new URL(`kyc-start/${kycId}`, exchange.baseUrl).href,
    {
      method: "POST",
      body: {},
    },
  );

  logger.info(`kyc-start resp status: ${startResp.status}`);
  logger.info(j2s(startResp.json()));

  // We need to "visit" the KYC proof URL at least once to trigger the exchange
  // asking for the KYC status.
  const proofUrl = new URL(`kyc-proof/MYPROV`, exchange.baseUrl);
  proofUrl.searchParams.set("state", paytoHash);
  proofUrl.searchParams.set("code", "code_is_ok");
  const proofHttpResp = await harnessHttpLib.fetch(proofUrl.href);
  logger.info(`proof resp status ${proofHttpResp.status}`);
  logger.info(`resp headers ${j2s(proofHttpResp.headers.toJSON())}`);
  if (
    !(proofHttpResp.status >= 200 && proofHttpResp.status <= 299) &&
    proofHttpResp.status !== 303
  ) {
    logger.error("kyc proof failed");
    logger.info(await proofHttpResp.text());
    t.assertTrue(false);
  }

  // Now that KYC is done, withdrawal should finally succeed.

  await withdrawalDoneCond;

  kycServer.stop();
}

runKycChallengerTest.suites = ["wallet"];
