/*
 This file is part of GNU Taler
 (C) 2019 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 {
  addPaytoQueryParams,
  Amounts,
  BankAccessApiClient,
  Configuration,
  decodeCrock,
  j2s,
  Logger,
  MerchantApiClient,
  rsaBlind,
  setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
import { clk } from "@gnu-taler/taler-util/clk";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import {
  CryptoDispatcher,
  downloadExchangeInfo,
  SynchronousCryptoWorkerFactoryPlain,
  topupReserveWithDemobank,
} from "@gnu-taler/taler-wallet-core";
import { deepStrictEqual } from "assert";
import fs from "fs";
import os from "os";
import path from "path";
import { runBench1 } from "./bench1.js";
import { runBench2 } from "./bench2.js";
import { runBench3 } from "./bench3.js";
import { runEnvFull } from "./env-full.js";
import { runEnv1 } from "./env1.js";
import { GlobalTestState, runTestWithState } from "./harness/harness.js";
import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
import { lintExchangeDeployment } from "./lint.js";
const logger = new Logger("taler-harness:index.ts");
process.on("unhandledRejection", (error: any) => {
  logger.error("unhandledRejection", error.message);
  logger.error("stack", error.stack);
  process.exit(2);
});
declare const __VERSION__: string;
function printVersion(): void {
  console.log(__VERSION__);
  process.exit(0);
}
export const testingCli = clk
  .program("testing", {
    help: "Command line interface for the GNU Taler test/deployment harness.",
  })
  .maybeOption("log", ["-L", "--log"], clk.STRING, {
    help: "configure log level (NONE, ..., TRACE)",
    onPresentHandler: (x) => {
      setGlobalLogLevelFromString(x);
    },
  })
  .flag("version", ["-v", "--version"], {
    onPresentHandler: printVersion,
  })
  .flag("verbose", ["-V", "--verbose"], {
    help: "Enable verbose output.",
  });
const advancedCli = testingCli.subcommand("advancedArgs", "advanced", {
  help: "Subcommands for advanced operations (only use if you know what you're doing!).",
});
advancedCli
  .subcommand("decode", "decode", {
    help: "Decode base32-crockford.",
  })
  .action((args) => {
    const enc = fs.readFileSync(0, "utf8");
    console.log(decodeCrock(enc.trim()));
  });
advancedCli
  .subcommand("bench1", "bench1", {
    help: "Run the 'bench1' benchmark",
  })
  .requiredOption("configJson", ["--config-json"], clk.STRING)
  .action(async (args) => {
    let config: any;
    try {
      config = JSON.parse(args.bench1.configJson);
    } catch (e) {
      console.log("Could not parse config JSON");
    }
    await runBench1(config);
  });
advancedCli
  .subcommand("bench2", "bench2", {
    help: "Run the 'bench2' benchmark",
  })
  .requiredOption("configJson", ["--config-json"], clk.STRING)
  .action(async (args) => {
    let config: any;
    try {
      config = JSON.parse(args.bench2.configJson);
    } catch (e) {
      console.log("Could not parse config JSON");
    }
    await runBench2(config);
  });
advancedCli
  .subcommand("bench3", "bench3", {
    help: "Run the 'bench3' benchmark",
  })
  .requiredOption("configJson", ["--config-json"], clk.STRING)
  .action(async (args) => {
    let config: any;
    try {
      config = JSON.parse(args.bench3.configJson);
    } catch (e) {
      console.log("Could not parse config JSON");
    }
    await runBench3(config);
  });
advancedCli
  .subcommand("envFull", "env-full", {
    help: "Run a test environment for bench1",
  })
  .action(async (args) => {
    const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env-full-"));
    const testState = new GlobalTestState({
      testDir,
    });
    await runTestWithState(testState, runEnvFull, "env-full", true);
  });
advancedCli
  .subcommand("env1", "env1", {
    help: "Run a test environment for bench1",
  })
  .action(async (args) => {
    const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-"));
    const testState = new GlobalTestState({
      testDir,
    });
    await runTestWithState(testState, runEnv1, "env1", true);
  });
const sandcastleCli = testingCli.subcommand("sandcastleArgs", "sandcastle", {
  help: "Subcommands for handling GNU Taler sandcastle deployments.",
});
const configCli = testingCli.subcommand("configArgs", "config", {
  help: "Subcommands for handling the Taler configuration.",
});
configCli.subcommand("show", "show").action(async (args) => {
  const config = Configuration.load();
  const cfgStr = config.stringify({
    diagnostics: true,
  });
  console.log(cfgStr);
});
configCli
  .subcommand("get", "get")
  .requiredArgument("section", clk.STRING)
  .requiredArgument("option", clk.STRING)
  .flag("file", ["-f"])
  .action(async (args) => {
    const config = Configuration.load();
    let res;
    if (args.get.file) {
      res = config.getPath(args.get.section, args.get.option);
    } else {
      res = config.getString(args.get.section, args.get.option);
    }
    if (res.isDefined()) {
      console.log(res.required());
    } else {
      console.warn("not found");
      process.exit(1);
    }
  });
const deploymentCli = testingCli.subcommand("deploymentArgs", "deployment", {
  help: "Subcommands for handling GNU Taler deployments.",
});
deploymentCli
  .subcommand("tipTopup", "tip-topup")
  .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING)
  .requiredOption("exchangeBaseUrl", ["--exchange-url"], clk.STRING)
  .requiredOption("merchantApikey", ["--merchant-apikey"], clk.STRING)
  .requiredOption("bankAccessUrl", ["--bank-access-url"], clk.STRING)
  .requiredOption("bankAccount", ["--bank-account"], clk.STRING)
  .requiredOption("bankPassword", ["--bank-password"], clk.STRING)
  .requiredOption("wireMethod", ["--wire-method"], clk.STRING)
  .requiredOption("amount", ["--amount"], clk.STRING)
  .action(async (args) => {
    const amount = args.tipTopup.amount;
    const merchantClient = new MerchantApiClient(
      args.tipTopup.merchantBaseUrl,
      {
        method: "token",
        token: args.tipTopup.merchantApikey,
      },
    );
    const res = await merchantClient.getPrivateInstanceInfo();
    console.log(res);
    const tipReserveResp = await merchantClient.createTippingReserve({
      exchange_url: args.tipTopup.exchangeBaseUrl,
      initial_balance: amount,
      wire_method: args.tipTopup.wireMethod,
    });
    console.log(tipReserveResp);
    const bankAccessApiClient = new BankAccessApiClient(
      args.tipTopup.bankAccessUrl,
      {
        auth: {
          username: args.tipTopup.bankAccount,
          password: args.tipTopup.bankPassword,
        },
      },
    );
    const paytoUri = addPaytoQueryParams(tipReserveResp.accounts[0].payto_uri, {
      message: `tip-reserve ${tipReserveResp.reserve_pub}`,
    });
    console.log("payto URI:", paytoUri);
    const transactions = await bankAccessApiClient.getTransactions(
      args.tipTopup.bankAccount,
    );
    console.log("transactions:", j2s(transactions));
    await bankAccessApiClient.createTransaction(args.tipTopup.bankAccount, {
      amount,
      paytoUri,
    });
  });
deploymentCli
  .subcommand("tipCleanup", "tip-cleanup")
  .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING)
  .requiredOption("merchantApikey", ["--merchant-apikey"], clk.STRING)
  .flag("dryRun", ["--dry-run"])
  .action(async (args) => {
    const merchantClient = new MerchantApiClient(
      args.tipCleanup.merchantBaseUrl,
      {
        method: "token",
        token: args.tipCleanup.merchantApikey,
      },
    );
    const res = await merchantClient.getPrivateInstanceInfo();
    console.log(res);
    const tipRes = await merchantClient.getPrivateTipReserves();
    console.log(tipRes);
    for (const reserve of tipRes.reserves) {
      if (Amounts.isZero(reserve.exchange_initial_amount)) {
        if (args.tipCleanup.dryRun) {
          logger.info(`dry run, would purge reserve ${reserve}`);
        } else {
          await merchantClient.deleteTippingReserve({
            reservePub: reserve.reserve_pub,
            purge: true,
          });
        }
      }
    }
    // FIXME: Now delete reserves that are not filled yet
  });
deploymentCli
  .subcommand("testTalerdotnetDemo", "test-demodottalerdotnet")
  .action(async (args) => {
    const http = createPlatformHttpLib();
    const cryptiDisp = new CryptoDispatcher(
      new SynchronousCryptoWorkerFactoryPlain(),
    );
    const cryptoApi = cryptiDisp.cryptoApi;
    const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
    const exchangeBaseUrl = "https://exchange.demo.taler.net/";
    const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
    await topupReserveWithDemobank({
      amount: "KUDOS:10",
      bankAccessApiBaseUrl:
        "https://bank.demo.taler.net/demobanks/default/access-api/",
      exchangeInfo,
      http,
      reservePub: reserveKeyPair.pub,
    });
    let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
    reserveUrl.searchParams.set("timeout_ms", "30000");
    console.log("requesting", reserveUrl.href);
    const longpollReq = http.fetch(reserveUrl.href, {
      method: "GET",
    });
    const reserveStatusResp = await longpollReq;
    console.log("reserve status", reserveStatusResp.status);
  });
deploymentCli
  .subcommand("testDemoTestdotdalerdotnet", "test-testdottalerdotnet")
  .action(async (args) => {
    const http = createPlatformHttpLib();
    const cryptiDisp = new CryptoDispatcher(
      new SynchronousCryptoWorkerFactoryPlain(),
    );
    const cryptoApi = cryptiDisp.cryptoApi;
    const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
    const exchangeBaseUrl = "https://exchange.test.taler.net/";
    const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
    await topupReserveWithDemobank({
      amount: "TESTKUDOS:10",
      bankAccessApiBaseUrl:
        "https://bank.test.taler.net/demobanks/default/access-api/",
      exchangeInfo,
      http,
      reservePub: reserveKeyPair.pub,
    });
    let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
    reserveUrl.searchParams.set("timeout_ms", "30000");
    console.log("requesting", reserveUrl.href);
    const longpollReq = http.fetch(reserveUrl.href, {
      method: "GET",
    });
    const reserveStatusResp = await longpollReq;
    console.log("reserve status", reserveStatusResp.status);
  });
deploymentCli
  .subcommand("testLocalhostDemo", "test-demo-localhost")
  .action(async (args) => {
    // Run checks against the "env-full" demo deployment on localhost
    const http = createPlatformHttpLib();
    const cryptiDisp = new CryptoDispatcher(
      new SynchronousCryptoWorkerFactoryPlain(),
    );
    const cryptoApi = cryptiDisp.cryptoApi;
    const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
    const exchangeBaseUrl = "http://localhost:8081/";
    const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
    await topupReserveWithDemobank({
      amount: "TESTKUDOS:10",
      bankAccessApiBaseUrl: "http://localhost:8082/taler-bank-access/",
      exchangeInfo,
      http,
      reservePub: reserveKeyPair.pub,
    });
    let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
    reserveUrl.searchParams.set("timeout_ms", "30000");
    console.log("requesting", reserveUrl.href);
    const longpollReq = http.fetch(reserveUrl.href, {
      method: "GET",
    });
    const reserveStatusResp = await longpollReq;
    console.log("reserve status", reserveStatusResp.status);
  });
deploymentCli
  .subcommand("tipStatus", "tip-status")
  .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING)
  .requiredOption("merchantApikey", ["--merchant-apikey"], clk.STRING)
  .action(async (args) => {
    const merchantClient = new MerchantApiClient(
      args.tipStatus.merchantBaseUrl,
      {
        method: "token",
        token: args.tipStatus.merchantApikey,
      },
    );
    const res = await merchantClient.getPrivateInstanceInfo();
    const tipRes = await merchantClient.getPrivateTipReserves();
    console.log(j2s(tipRes));
  });
deploymentCli
  .subcommand("lintExchange", "lint-exchange", {
    help: "Run checks on the exchange deployment.",
  })
  .flag("cont", ["--continue"], {
    help: "Continue after errors if possible",
  })
  .flag("debug", ["--debug"], {
    help: "Output extra debug info",
  })
  .action(async (args) => {
    await lintExchangeDeployment(
      args.lintExchange.debug,
      args.lintExchange.cont,
    );
  });
deploymentCli
  .subcommand("coincfg", "gen-coin-config", {
    help: "Generate a coin/denomination configuration for the exchange.",
  })
  .requiredOption("minAmount", ["--min-amount"], clk.STRING, {
    help: "Smallest denomination",
  })
  .requiredOption("maxAmount", ["--max-amount"], clk.STRING, {
    help: "Largest denomination",
  })
  .action(async (args) => {
    let out = "";
    const stamp = Math.floor(new Date().getTime() / 1000);
    const min = Amounts.parseOrThrow(args.coincfg.minAmount);
    const max = Amounts.parseOrThrow(args.coincfg.maxAmount);
    if (min.currency != max.currency) {
      console.error("currency mismatch");
      process.exit(1);
    }
    const currency = min.currency;
    let x = min;
    let n = 1;
    out += "# Coin configuration for the exchange.\n";
    out += '# Should be placed in "/etc/taler/conf.d/exchange-coins.conf".\n';
    out += "\n";
    while (Amounts.cmp(x, max) < 0) {
      out += `[COIN-${currency}-n${n}-t${stamp}]\n`;
      out += `VALUE = ${Amounts.stringify(x)}\n`;
      out += `DURATION_WITHDRAW = 7 days\n`;
      out += `DURATION_SPEND = 2 years\n`;
      out += `DURATION_LEGAL = 6 years\n`;
      out += `FEE_WITHDRAW = ${currency}:0\n`;
      out += `FEE_DEPOSIT = ${Amounts.stringify(min)}\n`;
      out += `FEE_REFRESH = ${currency}:0\n`;
      out += `FEE_REFUND = ${currency}:0\n`;
      out += `RSA_KEYSIZE = 2048\n`;
      out += `CIPHER = RSA\n`;
      out += "\n";
      x = Amounts.add(x, x).amount;
      n++;
    }
    console.log(out);
  });
const deploymentConfigCli = deploymentCli.subcommand("configArgs", "config", {
  help: "Subcommands the Taler configuration.",
});
deploymentConfigCli
  .subcommand("show", "show")
  .flag("diagnostics", ["-d", "--diagnostics"])
  .maybeArgument("cfgfile", clk.STRING, {})
  .action(async (args) => {
    const cfg = Configuration.load(args.show.cfgfile);
    console.log(
      cfg.stringify({
        diagnostics: args.show.diagnostics,
      }),
    );
  });
testingCli.subcommand("logtest", "logtest").action(async (args) => {
  logger.trace("This is a trace message.");
  logger.info("This is an info message.");
  logger.warn("This is an warning message.");
  logger.error("This is an error message.");
});
testingCli
  .subcommand("listIntegrationtests", "list-integrationtests")
  .action(async (args) => {
    for (const t of getTestInfo()) {
      let s = t.name;
      if (t.suites.length > 0) {
        s += ` (suites: ${t.suites.join(",")})`;
      }
      if (t.experimental) {
        s += ` [experimental]`;
      }
      console.log(s);
    }
  });
testingCli
  .subcommand("runIntegrationtests", "run-integrationtests")
  .maybeArgument("pattern", clk.STRING, {
    help: "Glob pattern to select which tests to run",
  })
  .maybeOption("suites", ["--suites"], clk.STRING, {
    help: "Only run selected suites (comma-separated list)",
  })
  .flag("dryRun", ["--dry"], {
    help: "Only print tests that will be selected to run.",
  })
  .flag("experimental", ["--experimental"], {
    help: "Include tests marked as experimental",
  })
  .flag("failFast", ["--fail-fast"], {
    help: "Exit after the first error",
  })
  .flag("waitOnFail", ["--wait-on-fail"], {
    help: "Exit after the first error",
  })
  .flag("quiet", ["--quiet"], {
    help: "Produce less output.",
  })
  .flag("noTimeout", ["--no-timeout"], {
    help: "Do not time out tests.",
  })
  .action(async (args) => {
    await runTests({
      includePattern: args.runIntegrationtests.pattern,
      failFast: args.runIntegrationtests.failFast,
      waitOnFail: args.runIntegrationtests.waitOnFail,
      suiteSpec: args.runIntegrationtests.suites,
      dryRun: args.runIntegrationtests.dryRun,
      verbosity: args.runIntegrationtests.quiet ? 0 : 1,
      includeExperimental: args.runIntegrationtests.experimental ?? false,
      noTimeout: args.runIntegrationtests.noTimeout,
    });
  });
async function read(stream: NodeJS.ReadStream) {
  const chunks = [];
  for await (const chunk of stream) chunks.push(chunk);
  return Buffer.concat(chunks).toString("utf8");
}
testingCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => {
  const data = await read(process.stdin);
  const lines = data.match(/[^\r\n]+/g);
  if (!lines) {
    throw Error("can't split lines");
  }
  const vals: Record = {};
  let inBlindSigningSection = false;
  for (const line of lines) {
    if (line === "blind signing:") {
      inBlindSigningSection = true;
      continue;
    }
    if (line[0] !== " ") {
      inBlindSigningSection = false;
      continue;
    }
    if (inBlindSigningSection) {
      const m = line.match(/  (\w+) (\w+)/);
      if (!m) {
        console.log("bad format");
        process.exit(2);
      }
      vals[m[1]] = m[2];
    }
  }
  console.log(vals);
  const req = (k: string) => {
    if (!vals[k]) {
      throw Error(`no value for ${k}`);
    }
    return decodeCrock(vals[k]);
  };
  const myBm = rsaBlind(
    req("message_hash"),
    req("blinding_key_secret"),
    req("rsa_public_key"),
  );
  deepStrictEqual(req("blinded_message"), myBm);
  console.log("check passed!");
});
export function main() {
  testingCli.run();
}