/*
 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 
 */
/**
 * Test harness for various GNU Taler components.
 * Also provides a fault-injection proxy.
 *
 * @author Florian Dold 
 */
/**
 * Imports
 */
import * as util from "util";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import * as http from "http";
import { ChildProcess, spawn } from "child_process";
import {
  Configuration,
  walletCoreApi,
  codec,
  AmountJson,
  Amounts,
} from "taler-wallet-core";
import { URL } from "url";
import axios from "axios";
import { talerCrypto, time } from "taler-wallet-core";
import { codecForMerchantOrderPrivateStatusResponse, codecForPostOrderResponse, PostOrderRequest, PostOrderResponse } from "./merchantApiTypes";
const exec = util.promisify(require("child_process").exec);
async function delay(ms: number): Promise {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(), ms);
  });
}
interface WaitResult {
  code: number | null;
  signal: NodeJS.Signals | null;
}
/**
 * Run a shell command, return stdout.
 */
export async function sh(command: string): Promise {
  console.log("runing command");
  console.log(command);
  return new Promise((resolve, reject) => {
    const stdoutChunks: Buffer[] = [];
    const proc = spawn(command, {
      stdio: ["inherit", "pipe", "inherit"],
      shell: true,
    });
    proc.stdout.on("data", (x) => {
      console.log("child process got data chunk");
      if (x instanceof Buffer) {
        stdoutChunks.push(x);
      } else {
        throw Error("unexpected data chunk type");
      }
    });
    proc.on("exit", (code) => {
      console.log("child process exited");
      if (code != 0) {
        reject(Error(`Unexpected exit code ${code} for '${command}'`));
        return;
      }
      const b = Buffer.concat(stdoutChunks).toString("utf-8");
      resolve(b);
    });
    proc.on("error", () => {
      reject(Error("Child process had error"));
    });
  });
}
export class ProcessWrapper {
  private waitPromise: Promise;
  constructor(public proc: ChildProcess) {
    this.waitPromise = new Promise((resolve, reject) => {
      proc.on("exit", (code, signal) => {
        resolve({ code, signal });
      });
      proc.on("error", (err) => {
        reject(err);
      });
    });
  }
  wait(): Promise {
    return this.waitPromise;
  }
}
export function makeTempDir(): Promise {
  return new Promise((resolve, reject) => {
    fs.mkdtemp(
      path.join(os.tmpdir(), "taler-integrationtest-"),
      (err, directory) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(directory);
        console.log(directory);
      },
    );
  });
}
interface CoinConfig {
  name: string;
  value: string;
  durationWithdraw: string;
  durationSpend: string;
  durationLegal: string;
  feeWithdraw: string;
  feeDeposit: string;
  feeRefresh: string;
  feeRefund: string;
  rsaKeySize: number;
}
const coinCommon = {
  durationLegal: "3 years",
  durationSpend: "2 years",
  durationWithdraw: "7 days",
  rsaKeySize: 1024,
};
const coin_ct1 = (curr: string): CoinConfig => ({
  ...coinCommon,
  name: `${curr}_ct1`,
  value: `${curr}:0.01`,
  feeDeposit: `${curr}:0.00`,
  feeRefresh: `${curr}:0.01`,
  feeRefund: `${curr}:0.00`,
  feeWithdraw: `${curr}:0.01`,
});
const coin_ct10 = (curr: string): CoinConfig => ({
  ...coinCommon,
  name: `${curr}_ct10`,
  value: `${curr}:0.10`,
  feeDeposit: `${curr}:0.01`,
  feeRefresh: `${curr}:0.01`,
  feeRefund: `${curr}:0.00`,
  feeWithdraw: `${curr}:0.01`,
});
const coin_u1 = (curr: string): CoinConfig => ({
  ...coinCommon,
  name: `${curr}_u1`,
  value: `${curr}:1`,
  feeDeposit: `${curr}:0.02`,
  feeRefresh: `${curr}:0.02`,
  feeRefund: `${curr}:0.02`,
  feeWithdraw: `${curr}:0.02`,
});
const coin_u2 = (curr: string): CoinConfig => ({
  ...coinCommon,
  name: `${curr}_u2`,
  value: `${curr}:2`,
  feeDeposit: `${curr}:0.02`,
  feeRefresh: `${curr}:0.02`,
  feeRefund: `${curr}:0.02`,
  feeWithdraw: `${curr}:0.02`,
});
const coin_u4 = (curr: string): CoinConfig => ({
  ...coinCommon,
  name: `${curr}_u4`,
  value: `${curr}:4`,
  feeDeposit: `${curr}:0.02`,
  feeRefresh: `${curr}:0.02`,
  feeRefund: `${curr}:0.02`,
  feeWithdraw: `${curr}:0.02`,
});
const coin_u8 = (curr: string): CoinConfig => ({
  ...coinCommon,
  name: `${curr}_u8`,
  value: `${curr}:8`,
  feeDeposit: `${curr}:0.16`,
  feeRefresh: `${curr}:0.16`,
  feeRefund: `${curr}:0.16`,
  feeWithdraw: `${curr}:0.16`,
});
const coin_u10 = (curr: string): CoinConfig => ({
  ...coinCommon,
  name: `${curr}_u10`,
  value: `${curr}:10`,
  feeDeposit: `${curr}:0.2`,
  feeRefresh: `${curr}:0.2`,
  feeRefund: `${curr}:0.2`,
  feeWithdraw: `${curr}:0.2`,
});
export class GlobalTestParams {
  testDir: string;
}
export class GlobalTestState {
  testDir: string;
  procs: ProcessWrapper[];
  servers: http.Server[];
  constructor(params: GlobalTestParams) {
    this.testDir = params.testDir;
    this.procs = [];
    this.servers = [];
    process.on("SIGINT", () => this.shutdownSync());
    process.on("SIGTERM", () => this.shutdownSync());
    process.on("unhandledRejection", () => this.shutdownSync());
    process.on("uncaughtException", () => this.shutdownSync());
  }
  assertTrue(b: boolean): asserts b {
    if (!b) {
      throw Error("test assertion failed");
    }
  }
  assertAmountEquals(
    amtExpected: string | AmountJson,
    amtActual: string | AmountJson,
  ): void {
    let ja1: AmountJson;
    let ja2: AmountJson;
    if (typeof amtExpected === "string") {
      ja1 = Amounts.parseOrThrow(amtExpected);
    } else {
      ja1 = amtExpected;
    }
    if (typeof amtActual === "string") {
      ja2 = Amounts.parseOrThrow(amtActual);
    } else {
      ja2 = amtActual;
    }
    if (Amounts.cmp(ja1, ja2) != 0) {
      throw Error(
        `test assertion failed: expected ${Amounts.stringify(
          ja1,
        )} but got ${Amounts.stringify(ja2)}`,
      );
    }
  }
  private shutdownSync(): void {
    for (const s of this.servers) {
      s.close();
      s.removeAllListeners();
    }
    for (const p of this.procs) {
      if (p.proc.exitCode == null) {
        p.proc.kill("SIGTERM");
      } else {
      }
    }
    console.log("*** test harness interrupted");
    console.log("*** test state can be found under", this.testDir);
    process.exit(1);
  }
  spawnService(command: string, logName: string): ProcessWrapper {
    const proc = spawn(command, {
      shell: true,
      stdio: ["inherit", "pipe", "pipe"],
    });
    const stderrLogFileName = this.testDir + `/${logName}-stderr.log`;
    const stderrLog = fs.createWriteStream(stderrLogFileName, {
      flags: "a",
    });
    proc.stderr.pipe(stderrLog);
    const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`;
    const stdoutLog = fs.createWriteStream(stdoutLogFileName, {
      flags: "a",
    });
    proc.stdout.pipe(stdoutLog);
    const procWrap = new ProcessWrapper(proc);
    this.procs.push(procWrap);
    return procWrap;
  }
  async terminate(): Promise {
    console.log("terminating");
    for (const s of this.servers) {
      s.close();
      s.removeAllListeners();
    }
    for (const p of this.procs) {
      if (p.proc.exitCode == null) {
        console.log("killing process", p.proc.pid);
        p.proc.kill("SIGTERM");
        await p.wait();
      }
    }
  }
}
export interface TalerConfigSection {
  options: Record;
}
export interface TalerConfig {
  sections: Record;
}
export interface DbInfo {
  connStr: string;
  dbname: string;
}
export async function setupDb(gc: GlobalTestState): Promise {
  const dbname = "taler-integrationtest";
  await exec(`dropdb "${dbname}" || true`);
  await exec(`createdb "${dbname}"`);
  return {
    connStr: `postgres:///${dbname}`,
    dbname,
  };
}
export interface BankConfig {
  currency: string;
  httpPort: number;
  database: string;
  suggestedExchange: string | undefined;
  suggestedExchangePayto: string | undefined;
  allowRegistrations: boolean;
}
function setPaths(config: Configuration, home: string) {
  config.setString("paths", "taler_home", home);
  config.setString(
    "paths",
    "taler_data_home",
    "$TALER_HOME/.local/share/taler/",
  );
  config.setString("paths", "taler_config_home", "$TALER_HOME/.config/taler/");
  config.setString("paths", "taler_cache_home", "$TALER_HOME/.config/taler/");
  config.setString(
    "paths",
    "taler_runtime_dir",
    "${TMPDIR:-${TMP:-/tmp}}/taler-system-runtime/",
  );
}
function setCoin(config: Configuration, c: CoinConfig) {
  const s = `coin_${c.name}`;
  config.setString(s, "value", c.value);
  config.setString(s, "duration_withdraw", c.durationWithdraw);
  config.setString(s, "duration_spend", c.durationSpend);
  config.setString(s, "duration_legal", c.durationLegal);
  config.setString(s, "fee_deposit", c.feeDeposit);
  config.setString(s, "fee_withdraw", c.feeWithdraw);
  config.setString(s, "fee_refresh", c.feeRefresh);
  config.setString(s, "fee_refund", c.feeRefund);
  config.setString(s, "rsa_keysize", `${c.rsaKeySize}`);
}
export class BankService {
  proc: ProcessWrapper | undefined;
  static async create(
    gc: GlobalTestState,
    bc: BankConfig,
  ): Promise {
    const config = new Configuration();
    setPaths(config, gc.testDir + "/talerhome");
    config.setString("taler", "currency", bc.currency);
    config.setString("bank", "database", bc.database);
    config.setString("bank", "http_port", `${bc.httpPort}`);
    config.setString("bank", "max_debt_bank", `${bc.currency}:999999`);
    config.setString(
      "bank",
      "allow_registrations",
      bc.allowRegistrations ? "yes" : "no",
    );
    if (bc.suggestedExchange) {
      config.setString("bank", "suggested_exchange", bc.suggestedExchange);
    }
    if (bc.suggestedExchangePayto) {
      config.setString(
        "bank",
        "suggested_exchange_payto",
        bc.suggestedExchangePayto,
      );
    }
    const cfgFilename = gc.testDir + "/bank.conf";
    config.write(cfgFilename);
    return new BankService(gc, bc, cfgFilename);
  }
  get port() {
    return this.bankConfig.httpPort;
  }
  private constructor(
    private globalTestState: GlobalTestState,
    private bankConfig: BankConfig,
    private configFile: string,
  ) {}
  async start(): Promise {
    this.proc = this.globalTestState.spawnService(
      `taler-bank-manage -c "${this.configFile}" serve-http`,
      "bank",
    );
  }
  async pingUntilAvailable(): Promise {
    const url = `http://localhost:${this.bankConfig.httpPort}/config`;
    while (true) {
      try {
        console.log("pinging bank");
        const resp = await axios.get(url);
        return;
      } catch (e) {
        console.log("bank not ready:", e.toString());
        await delay(1000);
      }
    }
  }
  async createAccount(username: string, password: string): Promise {
    const url = `http://localhost:${this.bankConfig.httpPort}/testing/register`;
    await axios.post(url, {
      username,
      password,
    });
  }
  async createRandomBankUser(): Promise {
    const bankUser: BankUser = {
      username:
        "user-" + talerCrypto.encodeCrock(talerCrypto.getRandomBytes(10)),
      password: "pw-" + talerCrypto.encodeCrock(talerCrypto.getRandomBytes(10)),
    };
    await this.createAccount(bankUser.username, bankUser.password);
    return bankUser;
  }
  async createWithdrawalOperation(
    bankUser: BankUser,
    amount: string,
  ): Promise {
    const url = `http://localhost:${this.bankConfig.httpPort}/accounts/${bankUser.username}/withdrawals`;
    const resp = await axios.post(
      url,
      {
        amount,
      },
      {
        auth: bankUser,
      },
    );
    return codecForWithdrawalOperationInfo().decode(resp.data);
  }
  async confirmWithdrawalOperation(
    bankUser: BankUser,
    wopi: WithdrawalOperationInfo,
  ): Promise {
    const url = `http://localhost:${this.bankConfig.httpPort}/accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`;
    await axios.post(
      url,
      {},
      {
        auth: bankUser,
      },
    );
  }
}
export interface BankUser {
  username: string;
  password: string;
}
export interface WithdrawalOperationInfo {
  withdrawal_id: string;
  taler_withdraw_uri: string;
}
const codecForWithdrawalOperationInfo = (): codec.Codec<
  WithdrawalOperationInfo
> =>
  codec
    .makeCodecForObject()
    .property("withdrawal_id", codec.codecForString)
    .property("taler_withdraw_uri", codec.codecForString)
    .build("WithdrawalOperationInfo");
export interface ExchangeConfig {
  name: string;
  currency: string;
  roundUnit?: string;
  httpPort: number;
  database: string;
}
export interface ExchangeServiceInterface {
  readonly baseUrl: string;
  readonly port: number;
  readonly name: string;
  readonly masterPub: string;
}
export class ExchangeService implements ExchangeServiceInterface {
  static create(gc: GlobalTestState, e: ExchangeConfig) {
    const config = new Configuration();
    config.setString("taler", "currency", e.currency);
    config.setString(
      "taler",
      "currency_round_unit",
      e.roundUnit ?? `${e.currency}:0.01`,
    );
    setPaths(config, gc.testDir + "/talerhome");
    config.setString(
      "exchange",
      "keydir",
      "${TALER_DATA_HOME}/exchange/live-keys/",
    );
    config.setString(
      "exchage",
      "revocation_dir",
      "${TALER_DATA_HOME}/exchange/revocations",
    );
    config.setString("exchange", "max_keys_caching", "forever");
    config.setString("exchange", "db", "postgres");
    config.setString(
      "exchange",
      "master_priv_file",
      "${TALER_DATA_HOME}/exchange/offline-keys/master.priv",
    );
    config.setString("exchange", "serve", "tcp");
    config.setString("exchange", "port", `${e.httpPort}`);
    config.setString("exchange", "port", `${e.httpPort}`);
    config.setString("exchange", "signkey_duration", "4 weeks");
    config.setString("exchange", "legal_duraction", "2 years");
    config.setString("exchange", "lookahead_sign", "32 weeks 1 day");
    config.setString("exchange", "lookahead_provide", "4 weeks 1 day");
    for (let i = 2020; i < 2029; i++) {
      config.setString(
        "fees-x-taler-bank",
        `wire-fee-${i}`,
        `${e.currency}:0.01`,
      );
      config.setString(
        "fees-x-taler-bank",
        `closing-fee-${i}`,
        `${e.currency}:0.01`,
      );
    }
    config.setString("exchangedb-postgres", "config", e.database);
    setCoin(config, coin_ct1(e.currency));
    setCoin(config, coin_ct10(e.currency));
    setCoin(config, coin_u1(e.currency));
    setCoin(config, coin_u2(e.currency));
    setCoin(config, coin_u4(e.currency));
    setCoin(config, coin_u8(e.currency));
    setCoin(config, coin_u10(e.currency));
    const exchangeMasterKey = talerCrypto.createEddsaKeyPair();
    config.setString(
      "exchange",
      "master_public_key",
      talerCrypto.encodeCrock(exchangeMasterKey.eddsaPub),
    );
    const masterPrivFile = config
      .getPath("exchange", "master_priv_file")
      .required();
    fs.mkdirSync(path.dirname(masterPrivFile), { recursive: true });
    fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv));
    console.log("writing key to", masterPrivFile);
    console.log("pub is", talerCrypto.encodeCrock(exchangeMasterKey.eddsaPub));
    console.log(
      "priv is",
      talerCrypto.encodeCrock(exchangeMasterKey.eddsaPriv),
    );
    const cfgFilename = gc.testDir + `/exchange-${e.name}.conf`;
    config.write(cfgFilename);
    return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey);
  }
  get masterPub() {
    return talerCrypto.encodeCrock(this.keyPair.eddsaPub);
  }
  get port() {
    return this.exchangeConfig.httpPort;
  }
  async setupTestBankAccount(
    bc: BankService,
    localName: string,
    accountName: string,
    password: string,
  ): Promise {
    await bc.createAccount(accountName, password);
    const config = Configuration.load(this.configFilename);
    config.setString(
      `exchange-account-${localName}`,
      "wire_response",
      `\${TALER_DATA_HOME}/exchange/account-${localName}.json`,
    );
    config.setString(
      `exchange-account-${localName}`,
      "payto_uri",
      `payto://x-taler-bank/localhost/${accountName}`,
    );
    config.setString(`exchange-account-${localName}`, "enable_credit", "yes");
    config.setString(`exchange-account-${localName}`, "enable_debit", "yes");
    config.setString(
      `exchange-account-${localName}`,
      "wire_gateway_url",
      `http://localhost:${bc.port}/taler-wire-gateway/${accountName}/`,
    );
    config.setString(
      `exchange-account-${localName}`,
      "wire_gateway_auth_method",
      "basic",
    );
    config.setString(`exchange-account-${localName}`, "username", accountName);
    config.setString(`exchange-account-${localName}`, "password", password);
    config.write(this.configFilename);
  }
  exchangeHttpProc: ProcessWrapper | undefined;
  exchangeWirewatchProc: ProcessWrapper | undefined;
  constructor(
    private globalState: GlobalTestState,
    private exchangeConfig: ExchangeConfig,
    private configFilename: string,
    private keyPair: talerCrypto.EddsaKeyPair,
  ) {}
  get name() {
    return this.exchangeConfig.name;
  }
  get baseUrl() {
    return `http://localhost:${this.exchangeConfig.httpPort}/`;
  }
  async start(): Promise {
    await exec(`taler-exchange-dbinit -c "${this.configFilename}"`);
    await exec(`taler-exchange-wire -c "${this.configFilename}"`);
    await exec(`taler-exchange-keyup -c "${this.configFilename}"`);
    this.exchangeWirewatchProc = this.globalState.spawnService(
      `taler-exchange-wirewatch -c "${this.configFilename}"`,
      `exchange-wirewatch-${this.name}`,
    );
    this.exchangeHttpProc = this.globalState.spawnService(
      `taler-exchange-httpd -c "${this.configFilename}"`,
      `exchange-httpd-${this.name}`,
    );
  }
  async pingUntilAvailable(): Promise {
    const url = `http://localhost:${this.exchangeConfig.httpPort}/keys`;
    while (true) {
      try {
        console.log("pinging exchange");
        const resp = await axios.get(url);
        console.log(resp.data);
        return;
      } catch (e) {
        console.log("exchange not ready:", e.toString());
        await delay(1000);
      }
    }
  }
}
export interface MerchantConfig {
  name: string;
  currency: string;
  httpPort: number;
  database: string;
}
export class MerchantService {
  proc: ProcessWrapper | undefined;
  constructor(
    private globalState: GlobalTestState,
    private merchantConfig: MerchantConfig,
    private configFilename: string,
  ) {}
  async start(): Promise {
    await exec(`taler-merchant-dbinit -c "${this.configFilename}"`);
    this.proc = this.globalState.spawnService(
      `taler-merchant-httpd -c "${this.configFilename}"`,
      `merchant-${this.merchantConfig.name}`,
    );
  }
  static async create(
    gc: GlobalTestState,
    mc: MerchantConfig,
  ): Promise {
    const config = new Configuration();
    config.setString("taler", "currency", mc.currency);
    config.setString("merchant", "serve", "tcp");
    config.setString("merchant", "port", `${mc.httpPort}`);
    config.setString("merchant", "db", "postgres");
    config.setString("exchangedb-postgres", "config", mc.database);
    const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`;
    config.write(cfgFilename);
    return new MerchantService(gc, mc, cfgFilename);
  }
  addExchange(e: ExchangeServiceInterface): void {
    const config = Configuration.load(this.configFilename);
    config.setString(
      `merchant-exchange-${e.name}`,
      "exchange_base_url",
      e.baseUrl,
    );
    config.setString(
      `merchant-exchange-${e.name}`,
      "currency",
      this.merchantConfig.currency,
    );
    config.setString(`merchant-exchange-${e.name}`, "master_key", e.masterPub);
    config.write(this.configFilename);
  }
  async addInstance(instanceConfig: MerchantInstanceConfig): Promise {
    if (!this.proc) {
      throw Error("merchant must be running to add instance");
    }
    console.log("adding instance");
    const url = `http://localhost:${this.merchantConfig.httpPort}/private/instances`;
    await axios.post(url, {
      payto_uris: instanceConfig.paytoUris,
      id: instanceConfig.id,
      name: instanceConfig.name,
      address: instanceConfig.address ?? {},
      jurisdiction: instanceConfig.jurisdiction ?? {},
      default_max_wire_fee:
        instanceConfig.defaultMaxWireFee ??
        `${this.merchantConfig.currency}:1.0`,
      default_wire_fee_amortization:
        instanceConfig.defaultWireFeeAmortization ?? 3,
      default_max_deposit_fee:
        instanceConfig.defaultMaxDepositFee ??
        `${this.merchantConfig.currency}:1.0`,
      default_wire_transfer_delay: instanceConfig.defaultWireTransferDelay ?? {
        d_ms: "forever",
      },
      default_pay_delay: instanceConfig.defaultPayDelay ?? { d_ms: "forever" },
    });
  }
  async queryPrivateOrderStatus(instanceName: string, orderId: string) {
    let url;
    if (instanceName === "default") {
      url = `http://localhost:${this.merchantConfig.httpPort}/private/orders/${orderId}`
    } else {
      url = `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/private/orders/${orderId}`;
    }
    const resp = await axios.get(url);
    return codecForMerchantOrderPrivateStatusResponse().decode(resp.data);
  }
  async createOrder(
    instanceName: string,
    req: PostOrderRequest,
  ): Promise {
    let url;
    if (instanceName === "default") {
      url = `http://localhost:${this.merchantConfig.httpPort}/private/orders`;
    } else {
      url = `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/private/orders`;
    }
    const resp = await axios.post(url, req);
    return codecForPostOrderResponse().decode(resp.data);
  }
  async pingUntilAvailable(): Promise {
    const url = `http://localhost:${this.merchantConfig.httpPort}/config`;
    while (true) {
      try {
        console.log("pinging merchant");
        const resp = await axios.get(url);
        console.log(resp.data);
        return;
      } catch (e) {
        console.log("merchant not ready", e.toString());
        await delay(1000);
      }
    }
  }
}
export interface MerchantInstanceConfig {
  id: string;
  name: string;
  paytoUris: string[];
  address?: unknown;
  jurisdiction?: unknown;
  defaultMaxWireFee?: string;
  defaultMaxDepositFee?: string;
  defaultWireFeeAmortization?: number;
  defaultWireTransferDelay?: time.Duration;
  defaultPayDelay?: time.Duration;
}
export function runTest(testMain: (gc: GlobalTestState) => Promise) {
  const main = async () => {
    const gc = new GlobalTestState({
      testDir: await makeTempDir(),
    });
    try {
      await testMain(gc);
    } finally {
      if (process.env["TALER_TEST_KEEP"] !== "1") {
        await gc.terminate();
        console.log("test logs and config can be found under", gc.testDir);
      }
    }
  };
  main().catch((e) => {
    console.error("FATAL: test failed with exception");
    if (e instanceof Error) {
      console.error(e);
    } else {
      console.error(e);
    }
    if (process.env["TALER_TEST_KEEP"] !== "1") {
      process.exit(1);
    }
  });
}
function shellWrap(s: string) {
  return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'";
}
export class WalletCli {
  constructor(private globalTestState: GlobalTestState) {}
  async apiRequest(
    request: string,
    payload: Record,
  ): Promise {
    const wdb = this.globalTestState.testDir + "/walletdb.json";
    const resp = await sh(
      `taler-wallet-cli --no-throttle --wallet-db '${wdb}' api '${request}' ${shellWrap(
        JSON.stringify(payload),
      )}`,
    );
    console.log(resp);
    return JSON.parse(resp) as walletCoreApi.CoreApiResponse;
  }
  async runUntilDone(): Promise {
    const wdb = this.globalTestState.testDir + "/walletdb.json";
    await sh(`taler-wallet-cli --no-throttle --wallet-db ${wdb} run-until-done`);
  }
  async runPending(): Promise {
    const wdb = this.globalTestState.testDir + "/walletdb.json";
    await sh(`taler-wallet-cli --no-throttle --wallet-db ${wdb} run-pending`);
  }
}