/*
 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 
 */
/**
 * Helpers to create typical test environments.
 *
 * @author Florian Dold 
 */
/**
 * Imports
 */
import {
  AmountString,
  TalerCorebankApiClient,
  ConfirmPayResultType,
  Duration,
  Logger,
  MerchantApiClient,
  MerchantContractTerms,
  NotificationType,
  PreparePayResultType,
  TransactionMajorState,
  WalletNotification,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "./denomStructures.js";
import {
  FaultInjectedExchangeService,
  FaultInjectedMerchantService,
} from "./faultInjection.js";
import {
  BankService,
  DbInfo,
  ExchangeService,
  ExchangeServiceInterface,
  FakebankService,
  GlobalTestState,
  HarnessExchangeBankAccount,
  MerchantService,
  MerchantServiceInterface,
  WalletCli,
  WalletClient,
  WalletService,
  WithAuthorization,
  generateRandomPayto,
  setupDb,
  setupSharedDb,
} from "./harness.js";
import * as fs from "fs";
const logger = new Logger("helpers.ts");
/**
 * @deprecated
 */
export interface SimpleTestEnvironment {
  commonDb: DbInfo;
  bank: BankService;
  exchange: ExchangeService;
  exchangeBankAccount: HarnessExchangeBankAccount;
  merchant: MerchantService;
  wallet: WalletCli;
}
/**
 * Improved version of the simple test environment,
 * with the daemonized wallet.
 */
export interface SimpleTestEnvironmentNg {
  commonDb: DbInfo;
  bank: BankService;
  exchange: ExchangeService;
  exchangeBankAccount: HarnessExchangeBankAccount;
  merchant: MerchantService;
  walletClient: WalletClient;
  walletService: WalletService;
}
export interface EnvOptions {
  /**
   * If provided, enable age restrictions with the specified age mask string.
   */
  ageMaskSpec?: string;
  mixedAgeRestriction?: boolean;
  additionalExchangeConfig?(e: ExchangeService): void;
  additionalMerchantConfig?(m: MerchantService): void;
  additionalBankConfig?(b: BankService): void;
}
export function getSharedTestDir(): string {
  return `/tmp/taler-harness@${process.env.USER}`;
}
export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
  const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
  const sharedDir = getSharedTestDir();
  fs.mkdirSync(sharedDir, { recursive: true });
  const db = await setupSharedDb(t);
  let bank: FakebankService;
  const prevSetupDone = fs.existsSync(sharedDir + "/setup-done");
  logger.info(`previous setup done: ${prevSetupDone}`);
  // Wallet has longer startup-time and no dependencies,
  // so we start it rather early.
  const walletStartProm = createWalletDaemonWithClient(t, { name: "wallet" });
  if (fs.existsSync(sharedDir + "/bank.conf")) {
    logger.info("reusing existing bank");
    bank = BankService.fromExistingConfig(t, {
      overridePath: sharedDir,
    });
  } else {
    logger.info("creating new bank config");
    bank = await BankService.create(t, {
      allowRegistrations: true,
      currency: "TESTKUDOS",
      database: db.connStr,
      httpPort: 8082,
      overrideTestDir: sharedDir,
    });
  }
  logger.info("setting up exchange");
  const exchangeName = "testexchange-1";
  const exchangeConfigFilename = sharedDir + `/exchange-${exchangeName}.conf`;
  logger.info(`exchange config filename: ${exchangeConfigFilename}`);
  let exchange: ExchangeService;
  if (fs.existsSync(exchangeConfigFilename)) {
    logger.info("reusing existing exchange config");
    exchange = ExchangeService.fromExistingConfig(t, exchangeName, {
      overridePath: sharedDir,
    });
  } else {
    logger.info("creating new exchange config");
    exchange = ExchangeService.create(t, {
      name: "testexchange-1",
      currency: "TESTKUDOS",
      httpPort: 8081,
      database: db.connStr,
      overrideTestDir: sharedDir,
    });
  }
  logger.info("setting up merchant");
  let merchant: MerchantService;
  const merchantName = "testmerchant-1";
  const merchantConfigFilename = sharedDir + `/merchant-${merchantName}}`;
  if (fs.existsSync(merchantConfigFilename)) {
    merchant = MerchantService.fromExistingConfig(t, merchantName, {
      overridePath: sharedDir,
    });
  } else {
    merchant = await MerchantService.create(t, {
      name: "testmerchant-1",
      currency: "TESTKUDOS",
      httpPort: 8083,
      database: db.connStr,
      overrideTestDir: sharedDir,
    });
  }
  logger.info("creating bank account for exchange");
  const exchangeBankAccount = await bank.createExchangeAccount(
    "myexchange",
    "x",
  );
  logger.info("creating exchange bank account");
  await exchange.addBankAccount("1", exchangeBankAccount);
  bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
  exchange.addCoinConfigList(coinConfig);
  merchant.addExchange(exchange);
  logger.info("basic setup done, starting services");
  if (!prevSetupDone) {
    // Must be done sequentially, due to a concurrency
    // issue in the *-dbinit tools.
    await exchange.dbinit();
    await merchant.dbinit();
  }
  const bankStart = async () => {
    await bank.start();
    await bank.pingUntilAvailable();
  };
  const exchangeStart = async () => {
    await exchange.start({
      skipDbinit: true,
      skipKeyup: prevSetupDone,
    });
    await exchange.pingUntilAvailable();
  };
  const merchStart = async () => {
    await merchant.start({
      skipDbinit: true,
    });
    await merchant.pingUntilAvailable();
    if (!prevSetupDone) {
      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 }),
        ),
      });
    }
  };
  await bankStart();
  const res = await Promise.all([
    exchangeStart(),
    merchStart(),
    undefined,
    walletStartProm,
  ]);
  const walletClient = res[3].walletClient;
  const walletService = res[3].walletService;
  fs.writeFileSync(sharedDir + "/setup-done", "OK");
  logger.info("setup done!");
  return {
    commonDb: db,
    exchange,
    merchant,
    walletClient,
    walletService,
    bank,
    exchangeBankAccount,
  };
}
/**
 * Run a test case with a simple TESTKUDOS Taler environment, consisting
 * of one exchange, one bank and one merchant.
 *
 * V2 uses a daemonized wallet instead of the CLI wallet.
 */
export async function createSimpleTestkudosEnvironmentV2(
  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",
  );
  await exchange.addBankAccount("1", exchangeBankAccount);
  bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
  if (opts.additionalBankConfig) {
    opts.additionalBankConfig(bank);
  }
  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);
  }
  if (opts.additionalExchangeConfig) {
    opts.additionalExchangeConfig(exchange);
  }
  await exchange.start();
  await exchange.pingUntilAvailable();
  merchant.addExchange(exchange);
  if (opts.additionalMerchantConfig) {
    opts.additionalMerchantConfig(merchant);
  }
  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 { walletClient, walletService } = await createWalletDaemonWithClient(
    t,
    { name: "wallet" },
  );
  console.log("setup done!");
  return {
    commonDb: db,
    exchange,
    merchant,
    walletClient,
    walletService,
    bank,
    exchangeBankAccount,
  };
}
export interface CreateWalletArgs {
  handleNotification?(wn: WalletNotification): void;
  name: string;
  persistent?: boolean;
}
export async function createWalletDaemonWithClient(
  t: GlobalTestState,
  args: CreateWalletArgs,
): Promise<{ walletClient: WalletClient; walletService: WalletService }> {
  const walletService = new WalletService(t, {
    name: args.name,
    useInMemoryDb: !args.persistent,
  });
  await walletService.start();
  await walletService.pingUntilAvailable();
  const walletClient = new WalletClient({
    unixPath: walletService.socketPath,
    onNotification(n) {
      console.log(`got ${args.name} notification`, n);
      if (args.handleNotification) {
        args.handleNotification(n);
      }
    },
  });
  await walletClient.connect();
  await walletClient.client.call(WalletApiOperation.InitWallet, {
    skipDefaults: true,
  });
  return { walletClient, walletService };
}
export interface FaultyMerchantTestEnvironment {
  commonDb: DbInfo;
  bank: BankService;
  exchange: ExchangeService;
  faultyExchange: FaultInjectedExchangeService;
  exchangeBankAccount: HarnessExchangeBankAccount;
  merchant: MerchantService;
  faultyMerchant: FaultInjectedMerchantService;
  walletClient: WalletClient;
}
/**
 * Run a test case with a simple TESTKUDOS Taler environment, consisting
 * of one exchange, one bank and one merchant.
 */
export async function createFaultInjectedMerchantTestkudosEnvironment(
  t: GlobalTestState,
): 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 faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083);
  const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081);
  // Base URL must contain port that the proxy is listening on.
  await exchange.modifyConfig(async (config) => {
    config.setString("exchange", "base_url", "http://localhost:9081/");
  });
  const exchangeBankAccount = await bank.createExchangeAccount(
    "myexchange",
    "x",
  );
  exchange.addBankAccount("1", exchangeBankAccount);
  bank.setSuggestedExchange(
    faultyExchange,
    exchangeBankAccount.accountPaytoUri,
  );
  await bank.start();
  await bank.pingUntilAvailable();
  exchange.addOfferedCoins(defaultCoinConfig);
  await exchange.start();
  await exchange.pingUntilAvailable();
  merchant.addExchange(faultyExchange);
  await merchant.start();
  await merchant.pingUntilAvailable();
  await merchant.addInstanceWithWireAccount({
    id: "default",
    name: "Default Instance",
    paytoUris: [generateRandomPayto("merchant-default")],
  });
  await merchant.addInstanceWithWireAccount({
    id: "minst1",
    name: "minst1",
    paytoUris: [generateRandomPayto("minst1")],
  });
  console.log("setup done!");
  const { walletClient } = await createWalletDaemonWithClient(t, {
    name: "default",
  });
  return {
    commonDb: db,
    exchange,
    merchant,
    walletClient,
    bank,
    exchangeBankAccount,
    faultyMerchant,
    faultyExchange,
  };
}
export interface WithdrawViaBankResult {
  withdrawalFinishedCond: Promise;
}
/**
 * Withdraw via a bank with the testing API enabled.
 * Uses the new notification-based mechanism to wait for the
 * operation to finish.
 */
export async function withdrawViaBankV2(
  t: GlobalTestState,
  p: {
    walletClient: WalletClient;
    bank: BankService;
    exchange: ExchangeServiceInterface;
    amount: AmountString;
    restrictAge?: number;
  },
): Promise {
  const { walletClient: wallet, bank, exchange, amount } = p;
  const bankClient = new TalerCorebankApiClient(bank.bankAccessApiBaseUrl);
  const user = await bankClient.createRandomBankUser();
  const wop = await bankClient.createWithdrawalOperation(user.username, amount);
  // Hand it to the wallet
  await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
    talerWithdrawUri: wop.taler_withdraw_uri,
    restrictAge: p.restrictAge,
  });
  // Withdraw (AKA select)
  const acceptRes = await wallet.client.call(
    WalletApiOperation.AcceptBankIntegratedWithdrawal,
    {
      exchangeBaseUrl: exchange.baseUrl,
      talerWithdrawUri: wop.taler_withdraw_uri,
      restrictAge: p.restrictAge,
    },
  );
  const withdrawalFinishedCond = wallet.waitForNotificationCond(
    (x) =>
      x.type === NotificationType.TransactionStateTransition &&
      x.newTxState.major === TransactionMajorState.Done &&
      x.transactionId === acceptRes.transactionId,
  );
  // Confirm it
  await bankClient.confirmWithdrawalOperation(user.username, {
    withdrawalOperationId: wop.withdrawal_id,
  });
  return {
    withdrawalFinishedCond,
  };
}
export async function applyTimeTravelV2(
  timetravelOffsetMs: number,
  s: {
    exchange?: ExchangeService;
    merchant?: MerchantService;
    walletClient?: WalletClient;
  },
): Promise {
  if (s.exchange) {
    await s.exchange.stop();
    s.exchange.setTimetravel(timetravelOffsetMs);
    await s.exchange.start();
    await s.exchange.pingUntilAvailable();
  }
  if (s.merchant) {
    await s.merchant.stop();
    s.merchant.setTimetravel(timetravelOffsetMs);
    await s.merchant.start();
    await s.merchant.pingUntilAvailable();
  }
  if (s.walletClient) {
    await s.walletClient.call(WalletApiOperation.TestingSetTimetravel, {
      offsetMs: timetravelOffsetMs,
    });
  }
}
/**
 * Make a simple payment and check that it succeeded.
 */
export async function makeTestPaymentV2(
  t: GlobalTestState,
  args: {
    merchant: MerchantServiceInterface;
    walletClient: WalletClient;
    order: Partial;
    instance?: string;
  },
  auth: WithAuthorization = {},
): Promise {
  // Set up order.
  const { walletClient, merchant, instance } = args;
  const merchantClient = new MerchantApiClient(
    merchant.makeInstanceBaseUrl(instance),
  );
  const orderResp = await merchantClient.createOrder({
    order: args.order,
  });
  let orderStatus = await merchantClient.queryPrivateOrderStatus({
    orderId: orderResp.order_id,
  });
  t.assertTrue(orderStatus.order_status === "unpaid");
  // Make wallet pay for the order
  const preparePayResult = await walletClient.call(
    WalletApiOperation.PreparePayForUri,
    {
      talerPayUri: orderStatus.taler_pay_uri,
    },
  );
  t.assertTrue(
    preparePayResult.status === PreparePayResultType.PaymentPossible,
  );
  const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
    proposalId: preparePayResult.proposalId,
  });
  t.assertTrue(r2.type === ConfirmPayResultType.Done);
  // Check if payment was successful.
  orderStatus = await merchantClient.queryPrivateOrderStatus({
    orderId: orderResp.order_id,
    instance,
  });
  t.assertTrue(orderStatus.order_status === "paid");
}