/*
 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 
 */
/**
 * Imports.
 */
import {
  Duration,
  j2s,
  Logger,
  NotificationType,
  TransactionMajorState,
  TransactionMinorState,
  TransactionType,
} from "@gnu-taler/taler-util";
import {
  BankAccessApi,
  BankApi,
  WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
  BankService,
  ExchangeService,
  getPayto,
  GlobalTestState,
  MerchantService,
  setupDb,
  WalletClient,
  WalletService,
} from "../harness/harness.js";
import { EnvOptions, SimpleTestEnvironmentNg } from "../harness/helpers.js";
import * as http from "node:http";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
const logger = new Logger("test-kyc.ts");
export async function createKycTestkudosEnvironment(
  t: GlobalTestState,
  coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
  opts: EnvOptions = {},
): Promise {
  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,
  });
  const exchangeBankAccount = await bank.createExchangeAccount(
    "myexchange",
    "x",
  );
  exchange.addBankAccount("1", exchangeBankAccount);
  bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
  await bank.start();
  await bank.pingUntilAvailable();
  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_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.addInstance({
    id: "default",
    name: "Default Instance",
    paytoUris: [getPayto("merchant-default")],
    defaultWireTransferDelay: Duration.toTalerProtocolDuration(
      Duration.fromSpec({ minutes: 1 }),
    ),
  });
  await merchant.addInstance({
    id: "minst1",
    name: "minst1",
    paytoUris: [getPayto("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({
    unixPath: walletService.socketPath,
    onNotification(n) {
      console.log("got notification", n);
    },
  });
  await walletClient.connect();
  await walletClient.client.call(WalletApiOperation.InitWallet, {
    skipDefaults: true,
  });
  console.log("setup done!");
  return {
    commonDb: db,
    exchange,
    merchant,
    walletClient,
    walletService,
    bank,
    exchangeBankAccount,
  };
}
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 {
  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 redirUri = new URL(redirUriUnparsed);
      redirUri.searchParams.set("code", "code_is_ok");
      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((resolve, reject) => {
    server.listen(6666, () => resolve());
  });
  return {
    stop() {
      server.close();
    },
  };
}
export async function runKycTest(t: GlobalTestState) {
  // Set up test environment
  const { walletClient, bank, exchange, merchant } =
    await createKycTestkudosEnvironment(t);
  const kycServer = await runTestfakeKycService();
  // Withdraw digital cash into the wallet.
  const amount = "TESTKUDOS:20";
  const user = await BankApi.createRandomBankUser(bank);
  const wop = await BankAccessApi.createWithdrawalOperation(bank, user, 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 BankApi.confirmWithdrawalOperation(bank, user, wop);
  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({
    allowHttp: true,
    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())}`);
  // Now that KYC is done, withdrawal should finally succeed.
  await withdrawalDoneCond;
  kycServer.stop();
}
runKycTest.suites = ["wallet"];