/*
 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 <http://www.gnu.org/licenses/>
 */

/**
 * Imports.
 */
import { deepStrictEqual } from "assert";
import fs from "fs";
import os from "os";
import path from "path";
import {
  addPaytoQueryParams,
  Amounts,
  Configuration,
  decodeCrock,
  j2s,
  Logger,
  parsePaytoUri,
  rsaBlind,
  setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
import { runBench1 } from "./bench1.js";
import { runBench2 } from "./bench2.js";
import { runBench3 } from "./bench3.js";
import { runEnv1 } from "./env1.js";
import {
  GlobalTestState,
  MerchantApiClient,
  MerchantPrivateApi,
  runTestWithState,
} from "./harness/harness.js";
import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
import { lintExchangeDeployment } from "./lint.js";
import { runEnvFull } from "./env-full.js";
import { clk } from "@gnu-taler/taler-util/clk";
import { createPlatformHttpLib } from "@gnu-taler/taler-util/http";
import { BankAccessApiClient } from "@gnu-taler/taler-wallet-core";

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 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({
      baseUrl: args.tipTopup.bankAccessUrl,
      username: args.tipTopup.bankAccount,
      password: args.tipTopup.bankPassword,
    });

    const paytoUri = addPaytoQueryParams(tipReserveResp.payto_uri, {
      message: `tip-reserve ${tipReserveResp.reserve_pub}`,
    });

    console.log("payto URI:", paytoUri);

    const transactions = await bankAccessApiClient.getTransactions();
    console.log("transactions:", j2s(transactions));

    await bankAccessApiClient.createTransaction({
      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("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();
    console.log(res);

    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.excludeByDefault) {
        s += ` [excluded by default]`;
      }
      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("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,
      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<string, string> = {};

  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();
}