/*
 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 {
  AbsoluteTime,
  AmountString,
  Duration,
  Logger,
  TalerBankConversionApi,
  TalerCorebankApiClient,
  TalerWireGatewayHttpClient,
  TransactionType,
  WithdrawalType,
  j2s,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import * as http from "node:http";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import { createWalletDaemonWithClient } from "../harness/environments.js";
import {
  BankService,
  ExchangeService,
  GlobalTestState,
  HarnessExchangeBankAccount,
  MerchantService,
  getTestHarnessPaytoForLabel,
  setupDb,
} from "../harness/harness.js";

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

interface TestfakeConversionService {
  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 runTestfakeConversionService(): Promise<TestfakeConversionService> {
  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 === "/config") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(
        JSON.stringify({
          version: "0:0:0",
          name: "taler-conversion-info",
          regional_currency: "FOO",
          fiat_currency: "BAR",
          regional_currency_specification: {
            alt_unit_names: {},
            name: "FOO",
            num_fractional_input_digits: 2,
            num_fractional_normal_digits: 2,
            num_fractional_trailing_zero_digits: 2,
          },
          fiat_currency_specification: {
            alt_unit_names: {},
            name: "BAR",
            num_fractional_input_digits: 2,
            num_fractional_normal_digits: 2,
            num_fractional_trailing_zero_digits: 2,
          },
          conversion_rate: {
            cashin_fee: "A:1" as AmountString,
            cashin_min_amount: "A:0.1" as AmountString,
            cashin_ratio: "1",
            cashin_rounding_mode: "zero",
            cashin_tiny_amount: "A:0.1" as AmountString,
            cashout_fee: "A:1" as AmountString,
            cashout_min_amount: "A:0.1" as AmountString,
            cashout_ratio: "1",
            cashout_rounding_mode: "zero",
            cashout_tiny_amount: "A:0.1" as AmountString,
          },
        } satisfies TalerBankConversionApi.TalerConversionInfoConfig),
      );
    } else if (path === "/cashin-rate") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(
        JSON.stringify({
          amount_debit: "FOO:123",
          amount_credit: "BAR:123",
        }),
      );
    } 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(8071, () => resolve());
  });
  return {
    stop() {
      server.close();
    },
  };
}

/**
 * Test for currency conversion during manual withdrawal.
 */
export async function runWithdrawalConversionTest(t: GlobalTestState) {
  // Set up test environment

  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",
    httpPort: 8083,
    database: db.connStr,
  });

  let exchangeBankAccount: HarnessExchangeBankAccount = {
    wireGatewayApiBaseUrl: new URL(
      "accounts/myexchange/taler-wire-gateway/",
      bank.corebankApiBaseUrl,
    ).href,
    wireGatewayAuth: {
      username: "myexchange",
      password: "password",
    },
    accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"),
    conversionUrl: "http://localhost:8071/",
  };

  await exchange.addBankAccount("1", exchangeBankAccount);

  await bank.start();

  await bank.pingUntilAvailable();

  const bankClientAuth = {
    username: "admin",
    password: "admin-password",
  };

  const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
    auth: bankClientAuth,
  });

  await bankClient.registerAccountExtended({
    name: exchangeBankAccount.wireGatewayAuth.username,
    username: exchangeBankAccount.wireGatewayAuth.username,
    password: exchangeBankAccount.wireGatewayAuth.password,
    is_taler_exchange: true,
    payto_uri: exchangeBankAccount.accountPaytoUri,
  });

  exchange.addOfferedCoins(defaultCoinConfig);

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

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

  const { accessToken: adminAccessToken } = await merchant.addInstanceWithWireAccount({
    id: "admin",
    name: "Default Instance",
    paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
    defaultWireTransferDelay: Duration.toTalerProtocolDuration(
      Duration.fromSpec({ minutes: 1 }),
    ),
  });

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

  const { walletClient } = await createWalletDaemonWithClient(t, {
    name: "wallet",
  });

  await runTestfakeConversionService();

  // Create a withdrawal operation

  const user = await bankClient.createRandomBankUser();

  await walletClient.call(WalletApiOperation.AddExchange, {
    exchangeBaseUrl: exchange.baseUrl,
  });

  const infoRes = await walletClient.call(
    WalletApiOperation.GetWithdrawalDetailsForAmount,
    {
      exchangeBaseUrl: exchange.baseUrl,
      amount: "TESTKUDOS:20" as AmountString,
    },
  );

  console.log(`withdrawal details: ${j2s(infoRes)}`);

  const checkTransferAmount = infoRes.withdrawalAccountsList[0].transferAmount;
  t.assertTrue(checkTransferAmount != null);
  t.assertAmountEquals(checkTransferAmount, "FOO:123");

  const tStart = AbsoluteTime.now();

  logger.info("starting AcceptManualWithdrawal request");
  // We expect this to return immediately.

  const wres = await walletClient.call(
    WalletApiOperation.AcceptManualWithdrawal,
    {
      exchangeBaseUrl: exchange.baseUrl,
      amount: "TESTKUDOS:10" as AmountString,
    },
  );

  logger.info("AcceptManualWithdrawal finished");
  logger.info(`result: ${j2s(wres)}`);

  const acceptedTransferAmount = wres.withdrawalAccountsList[0].transferAmount;
  t.assertTrue(acceptedTransferAmount != null);

  t.assertAmountEquals(acceptedTransferAmount, "FOO:123");

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

  t.assertDeepEqual(txInfo.type, TransactionType.Withdrawal);
  t.assertDeepEqual(
    txInfo.withdrawalDetails.type,
    WithdrawalType.ManualTransfer,
  );
  t.assertTrue(!!txInfo.withdrawalDetails.exchangeCreditAccountDetails);
  t.assertDeepEqual(
    txInfo.withdrawalDetails.exchangeCreditAccountDetails[0].transferAmount,
    "FOO:123",
  );

  // Check that the request did not go into long-polling.
  const duration = AbsoluteTime.difference(tStart, AbsoluteTime.now());
  if (typeof duration.d_ms !== "number" || duration.d_ms > 5 * 1000) {
    throw Error("withdrawal took too long (longpolling issue)");
  }

  const reservePub: string = wres.reservePub;

  const wireGatewayApiClient = new TalerWireGatewayHttpClient(
    exchangeBankAccount.wireGatewayApiBaseUrl,
  );

  await wireGatewayApiClient.addIncoming({
    auth: bank.getAdminAuth(),
    body: {
      amount: "TESTKUDOS:10",
      debit_account: user.accountPaytoUri,
      reserve_pub: reservePub,
    },
  });

  await exchange.runWirewatchOnce();

  await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});

  // Check balance

  const balResp = await walletClient.call(WalletApiOperation.GetBalances, {});
  t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
}

runWithdrawalConversionTest.suites = ["wallet"];
