/*
 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 {
  Duration,
  Logger,
  NotificationType,
  TalerCorebankApiClient,
  TransactionMajorState,
  TransactionMinorState,
  TransactionType,
  j2s,
} from "@gnu-taler/taler-util";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as http from "node:http";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
    BankService,
  ExchangeService,
  GlobalTestState,
  MerchantService,
  WalletClient,
  WalletService,
  generateRandomPayto,
  setupDb,
} from "../harness/harness.js";
import { EnvOptions, SimpleTestEnvironmentNg3 } from "../harness/helpers.js";

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

export async function createKycTestkudosEnvironment(
  t: GlobalTestState,
  coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
  opts: EnvOptions = {},
): Promise<SimpleTestEnvironmentNg3> {
  const db = await setupDb(t);

  const bank = await BankService.create(t, {
    allowRegistrations: true,
    currency: "TESTKUDOS",
    database: db.connStr,
    httpPort: 8082,
  });

  const exchange = ExchangeService.create(t, {
    name: "testexchange-1",
    currency: "TESTKUDOS",
    httpPort: 8081,
    database: db.connStr,
  });

  const merchant = await MerchantService.create(t, {
    name: "testmerchant-1",
    currency: "TESTKUDOS",
    httpPort: 8083,
    database: db.connStr,
  });

  let receiverName = "Exchange";
  let exchangeBankUsername = "exchange";
  let exchangeBankPassword = "mypw";
  let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);

  await exchange.addBankAccount("1", {
    accountName: exchangeBankUsername,
    accountPassword: exchangeBankPassword,
    wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href,
    accountPaytoUri: exchangePaytoUri,
  });

  bank.setSuggestedExchange(exchange, exchangePaytoUri);

  await bank.start();

  await bank.pingUntilAvailable();

  const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
    auth: {
      username: "admin",
      password: "adminpw",
    },
  });

  await bankClient.registerAccountExtended({
    name: receiverName,
    password: exchangeBankPassword,
    username: exchangeBankUsername,
    is_taler_exchange: true,
    payto_uri: exchangePaytoUri,
  });

  const ageMaskSpec = opts.ageMaskSpec;

  if (ageMaskSpec) {
    exchange.enableAgeRestrictions(ageMaskSpec);
    // Enable age restriction for all coins.
    exchange.addCoinConfigList(
      coinConfig.map((x) => ({
        ...x,
        name: `${x.name}-age`,
        ageRestricted: true,
      })),
    );
    // For mixed age restrictions, we also offer coins without age restrictions
    if (opts.mixedAgeRestriction) {
      exchange.addCoinConfigList(
        coinConfig.map((x) => ({ ...x, ageRestricted: false })),
      );
    }
  } else {
    exchange.addCoinConfigList(coinConfig);
  }

  await exchange.modifyConfig(async (config) => {
    const myprov = "kyc-provider-myprov";
    config.setString(myprov, "cost", "0");
    config.setString(myprov, "logic", "oauth2");
    config.setString(myprov, "provided_checks", "dummy1");
    config.setString(myprov, "user_type", "individual");
    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",
    );
    config.setString(
      "kyc-legitimization-withdraw1",
      "required_checks",
      "dummy1",
    );
    config.setString("kyc-legitimization-withdraw1", "timeframe", "1d");
    config.setString(
      "kyc-legitimization-withdraw1",
      "threshold",
      "TESTKUDOS:5",
    );
  });

  await exchange.start();
  await exchange.pingUntilAvailable();

  merchant.addExchange(exchange);

  await merchant.start();
  await merchant.pingUntilAvailable();

  await merchant.addInstanceWithWireAccount({
    id: "default",
    name: "Default Instance",
    paytoUris: [generateRandomPayto("merchant-default")],
    defaultWireTransferDelay: Duration.toTalerProtocolDuration(
      Duration.fromSpec({ minutes: 1 }),
    ),
  });

  await merchant.addInstanceWithWireAccount({
    id: "minst1",
    name: "minst1",
    paytoUris: [generateRandomPayto("minst1")],
    defaultWireTransferDelay: Duration.toTalerProtocolDuration(
      Duration.fromSpec({ minutes: 1 }),
    ),
  });

  const walletService = new WalletService(t, {
    name: "wallet",
    useInMemoryDb: true,
  });
  await walletService.start();
  await walletService.pingUntilAvailable();

  const walletClient = new WalletClient({
    name: "wallet",
    unixPath: walletService.socketPath,
    onNotification(n) {
      console.log("got notification", n);
    },
  });
  await walletClient.connect();
  await walletClient.client.call(WalletApiOperation.InitWallet, {
    config: {
      testing: {
        skipDefaults: true,
      },
    },
  });

  console.log("setup done!");

  return {
    commonDb: db,
    exchange,
    merchant,
    walletClient,
    walletService,
    bankClient,
    exchangeBankAccount: {
      accountName: '',
      accountPassword: '',
      accountPaytoUri: '',
      wireGatewayApiBaseUrl: '',
    },
  };
}

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",
          },
        }),
      );
    } 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 runKycTest(t: GlobalTestState) {
  // Set up test environment

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

  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 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 kycUrl = txState.kycUrl;

  t.assertTrue(!!kycUrl);

  logger.info(`kyc URL is ${kycUrl}`);

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

  const httpLib = createPlatformHttpLib({
    enableThrottling: false,
  });
  const kycServerResp = await httpLib.fetch(kycUrl);
  const kycLoginResp = await kycServerResp.json();
  logger.info(`kyc server resp: ${j2s(kycLoginResp)}`);
  const kycProofUrl = kycLoginResp.redirect_uri;
  // We need to "visit" the KYC proof URL at least once to trigger the exchange
  // asking for the KYC status.
  const proofHttpResp = await httpLib.fetch(kycProofUrl);
  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();
}

runKycTest.suites = ["wallet"];
