/*
  This file is part of TALER
  (C) 2014--2023 Taler Systems SA

  TALER is free software; you can redistribute it and/or modify it
  under the terms of the GNU Affero General Public License as
  published by the Free Software Foundation; either version 3, or
  (at your option) any later version.

  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 TALER; see the file COPYING.  If not,
  see <http://www.gnu.org/licenses/>
*/
/**
 * @file taler-merchant-benchmark.c
 * @brief benchmark the backend to evaluate performance
 * @author Marcello Stanisci
 * @author Christian Grothoff
 */
#include "platform.h"
#include <taler/taler_util.h>
#include <taler/taler_testing_lib.h>
#include "taler_merchant_util.h"
#include "taler_merchant_testing_lib.h"


/**
 * Maximum length of an amount (value plus currency string) needed by the test.
 * We have a 32-bit and a 64-bit value (~48 characters), plus the currency, plus
 * some punctuation.
 */
#define MAX_AMOUNT_LEN (TALER_CURRENCY_LEN + 64)

/**
 * Maximum length of an order JSON.  Generously allocated.
 */
#define MAX_ORDER_LEN (MAX_AMOUNT_LEN * 4 + 2048)


/**
 * ID to use for the 'alternative' instance.
 */
static const char *alt_instance_id = "alt";

/**
 * What API key should we send in the HTTP 'Authorization' header?
 */
static char *apikey;

/**
 * Witnesses if the ordinary cases payment suite should be run.
 */
static bool ordinary;

/**
 * Witnesses if the corner cases payment suite should be run.
 */
static bool corner;

/**
 * Base URL of the alternative non default instance.
 */
static char *alt_instance_url;

/**
 * How many unaggregated payments we want to generate.
 */
static unsigned int unaggregated_number = 1;

/**
 * How many payments that use two coins we want to generate.
 */
static unsigned int twocoins_number = 1;

/**
 * How many payments we want to generate.
 */
static unsigned int payments_number = 1;

/**
 * Config filename to give to commands (like wirewatch).
 */
static char *cfg_filename;

/**
 * Merchant base URL.
 */
static char *merchant_url;

/**
 * Currency used.
 */
static char *currency;

/**
 * Set to 1 if `-f` command line option given.
 */
static int use_fakebank;

/**
 * Configuration section with details about the exchange
 * bank account to use.
 */
static char *exchange_bank_section;

/**
 * Credentials to use for the benchmark.
 */
static struct TALER_TESTING_Credentials cred;


/**
 * Actual commands collection.
 */
static void
run (void *cls,
     struct TALER_TESTING_Interpreter *is)
{
  char CURRENCY_10_02[MAX_AMOUNT_LEN];
  char CURRENCY_10[MAX_AMOUNT_LEN];
  char CURRENCY_9_98[MAX_AMOUNT_LEN];
  char CURRENCY_5_01[MAX_AMOUNT_LEN];
  char CURRENCY_5[MAX_AMOUNT_LEN];
  char CURRENCY_4_99[MAX_AMOUNT_LEN];
  char CURRENCY_4_98[MAX_AMOUNT_LEN];
  char CURRENCY_0_02[MAX_AMOUNT_LEN];
  char CURRENCY_0_01[MAX_AMOUNT_LEN];

  GNUNET_snprintf (CURRENCY_10_02,
                   sizeof (CURRENCY_10_02),
                   "%s:10.02",
                   currency);
  GNUNET_snprintf (CURRENCY_10,
                   sizeof (CURRENCY_10),
                   "%s:10",
                   currency);
  GNUNET_snprintf (CURRENCY_9_98,
                   sizeof (CURRENCY_9_98),
                   "%s:9.98",
                   currency);
  GNUNET_snprintf (CURRENCY_5_01,
                   sizeof (CURRENCY_5_01),
                   "%s:5.01",
                   currency);
  GNUNET_snprintf (CURRENCY_5,
                   sizeof (CURRENCY_5),
                   "%s:5",
                   currency);
  GNUNET_snprintf (CURRENCY_4_99,
                   sizeof (CURRENCY_4_99),
                   "%s:4.99",
                   currency);
  GNUNET_snprintf (CURRENCY_4_98,
                   sizeof (CURRENCY_4_98),
                   "%s:4.98",
                   currency);
  GNUNET_snprintf (CURRENCY_0_02,
                   sizeof (CURRENCY_0_02),
                   "%s:0.02",
                   currency);
  GNUNET_snprintf (CURRENCY_0_01,
                   sizeof (CURRENCY_0_01),
                   "%s:0.01",
                   currency);
  if (ordinary)
  {
    struct TALER_TESTING_Command ordinary_commands[] = {
      TALER_TESTING_cmd_get_exchange (
        "get-exchange",
        cred.cfg,
        NULL,
        true,
        true),
      TALER_TESTING_cmd_set_authorization (
        "set-auth-valid",
        apikey),
      TALER_TESTING_cmd_merchant_post_instances (
        "instance-create-admin",
        merchant_url,
        "admin",
        MHD_HTTP_NO_CONTENT),
      TALER_TESTING_cmd_merchant_post_account (
        "instance-create-default-account",
        merchant_url,
        cred.user42_payto,
        NULL, NULL,
        MHD_HTTP_OK),
      TALER_TESTING_cmd_admin_add_incoming (
        "create-reserve-1",
        CURRENCY_10_02,
        &cred.ba,
        cred.user43_payto),
      TALER_TESTING_cmd_exec_wirewatch2 (
        "wirewatch-1",
        cfg_filename,
        exchange_bank_section),
      TALER_TESTING_cmd_withdraw_amount (
        "withdraw-coin-1",
        "create-reserve-1",
        CURRENCY_5,
        0,
        MHD_HTTP_OK),
      TALER_TESTING_cmd_withdraw_amount (
        "withdraw-coin-2",
        "create-reserve-1",
        CURRENCY_5,
        0,
        MHD_HTTP_OK),
      TALER_TESTING_cmd_merchant_post_orders (
        "create-proposal-1",
        cred.cfg,
        merchant_url,
        MHD_HTTP_OK,
        NULL,  /* random order ID please */
        GNUNET_TIME_UNIT_ZERO_TS,
        GNUNET_TIME_UNIT_FOREVER_TS,
        CURRENCY_5),
      TALER_TESTING_cmd_merchant_pay_order (
        "deposit-simple",
        merchant_url,
        MHD_HTTP_OK,
        "create-proposal-1",
        "withdraw-coin-1",
        CURRENCY_5,
        CURRENCY_4_99,
        NULL),
      TALER_TESTING_cmd_rewind_ip (
        "rewind-payments",
        "create-reserve-1",
        payments_number),
      TALER_TESTING_cmd_exec_aggregator (
        "aggregate-1x",
        cfg_filename),
      TALER_TESTING_cmd_exec_transfer (
        "transfer-1",
        cfg_filename),

      TALER_TESTING_cmd_end ()
    };

    TALER_TESTING_run (is,
                       ordinary_commands);
    return;
  }

  if (corner) /* should never be 'false' here */
  {
    struct TALER_TESTING_Command corner_commands[] = {
      TALER_TESTING_cmd_get_exchange (
        "get-exchange",
        cred.cfg,
        NULL,
        true,
        true),
      TALER_TESTING_cmd_set_authorization (
        "set-auth-valid",
        apikey),
      TALER_TESTING_cmd_merchant_post_instances (
        "instance-create-admin",
        merchant_url,
        "admin",
        MHD_HTTP_NO_CONTENT),
      TALER_TESTING_cmd_merchant_post_account (
        "instance-create-default-account",
        merchant_url,
        cred.user42_payto,
        NULL, NULL,
        MHD_HTTP_OK),
      TALER_TESTING_cmd_merchant_post_instances (
        "instance-create-alt",
        merchant_url,
        alt_instance_id,
        MHD_HTTP_NO_CONTENT),
      TALER_TESTING_cmd_merchant_post_account (
        "instance-create-alt-account",
        alt_instance_url,
        cred.user42_payto,
        NULL, NULL,
        MHD_HTTP_OK),
      TALER_TESTING_cmd_admin_add_incoming (
        "create-reserve-1",
        CURRENCY_5_01,
        &cred.ba,
        cred.user43_payto),
      TALER_TESTING_cmd_exec_wirewatch2 (
        "wirewatch-1",
        cfg_filename,
        exchange_bank_section),
      TALER_TESTING_cmd_withdraw_amount (
        "withdraw-coin-1",
        "create-reserve-1",
        CURRENCY_5,
        0,
        MHD_HTTP_OK),
      TALER_TESTING_cmd_merchant_post_orders (
        "create-unaggregated-proposal",
        cred.cfg,
        alt_instance_url,
        MHD_HTTP_OK,
        NULL,  /* use random order ID */
        GNUNET_TIME_UNIT_ZERO_TS,
        GNUNET_TIME_UNIT_FOREVER_TS,
        CURRENCY_5),
      TALER_TESTING_cmd_merchant_pay_order (
        "deposit-unaggregated",
        alt_instance_url,
        MHD_HTTP_OK,
        "create-unaggregated-proposal",
        "withdraw-coin-1",
        CURRENCY_5,
        CURRENCY_4_99,
        NULL),
      TALER_TESTING_cmd_rewind_ip (
        "rewind-unaggregated",
        "create-reserve-1",
        unaggregated_number),
      TALER_TESTING_cmd_admin_add_incoming (
        "create-reserve-2",
        CURRENCY_10_02,
        &cred.ba,
        cred.user43_payto),
      TALER_TESTING_cmd_exec_wirewatch2 (
        "wirewatch-2",
        cfg_filename,
        exchange_bank_section),
      TALER_TESTING_cmd_withdraw_amount (
        "withdraw-coin-2",
        "create-reserve-2",
        CURRENCY_5,
        0,
        MHD_HTTP_OK),
      TALER_TESTING_cmd_withdraw_amount (
        "withdraw-coin-3",
        "create-reserve-2",
        CURRENCY_5,
        0,
        MHD_HTTP_OK),
      TALER_TESTING_cmd_merchant_post_orders (
        "create-twocoins-proposal",
        cred.cfg,
        merchant_url,
        MHD_HTTP_OK,
        NULL, /* use random order ID */
        GNUNET_TIME_UNIT_ZERO_TS,
        GNUNET_TIME_UNIT_FOREVER_TS,
        CURRENCY_10),
      TALER_TESTING_cmd_merchant_pay_order (
        "deposit-twocoins",
        merchant_url,
        MHD_HTTP_OK,
        "create-twocoins-proposal",
        "withdraw-coin-2;withdraw-coin-3",
        CURRENCY_10,
        CURRENCY_9_98,
        NULL),
      TALER_TESTING_cmd_exec_aggregator (
        "aggregate-twocoins",
        cfg_filename),
      TALER_TESTING_cmd_exec_transfer (
        "transfer-twocoins",
        cfg_filename),
      TALER_TESTING_cmd_rewind_ip (
        "rewind-twocoins",
        "create-reserve-2",
        twocoins_number),
      TALER_TESTING_cmd_end ()
    };

    TALER_TESTING_run (is,
                       corner_commands);
    return;
  }
}


/**
 * The main function of the serve tool
 *
 * @param argc number of arguments from the command line
 * @param argv command line arguments
 * @return 0 ok, or `enum PaymentGeneratorError` on error
 */
int
main (int argc,
      char *const *argv)
{
  char *loglev = NULL;
  char *logfile = NULL;
  char *exchange_account = NULL;
  struct GNUNET_GETOPT_CommandLineOption *options;
  struct GNUNET_GETOPT_CommandLineOption root_options[] = {
    GNUNET_GETOPT_option_cfgfile (&cfg_filename),
    GNUNET_GETOPT_option_string (
      'u',
      "exchange-account-section",
      "SECTION",
      "use exchange bank account configuration from the given SECTION",
      &exchange_bank_section),
    GNUNET_GETOPT_option_flag (
      'f',
      "fakebank",
      "use fakebank for the banking system",
      &use_fakebank),
    GNUNET_GETOPT_option_version (PACKAGE_VERSION " " VCS_VERSION),
    GNUNET_GETOPT_option_help (
      TALER_MERCHANT_project_data (),
      "Runs benchmark logic against merchant backend. "
      "Must be used with either 'ordinary' or 'corner' sub-commands."),
    GNUNET_GETOPT_option_string (
      'l',
      "logfile",
      "LF",
      "will log to file LF",
      &logfile),
    GNUNET_GETOPT_option_loglevel (&loglev),
    GNUNET_GETOPT_OPTION_END
  };
  struct GNUNET_GETOPT_CommandLineOption corner_options[] = {
    GNUNET_GETOPT_option_string (
      'a',
      "apikey",
      "APIKEY",
      "HTTP 'Authorization' header to send to the merchant",
      &apikey),
    GNUNET_GETOPT_option_cfgfile (&cfg_filename),
    GNUNET_GETOPT_option_flag (
      'f',
      "fakebank",
      "use fakebank for the banking system",
      &use_fakebank),
    GNUNET_GETOPT_option_help (
      TALER_MERCHANT_project_data (),
      "Populate databases with corner case payments"),
    GNUNET_GETOPT_option_string (
      'l',
      "logfile",
      "LF",
      "will log to file LF",
      &logfile),
    GNUNET_GETOPT_option_loglevel (&loglev),
    GNUNET_GETOPT_option_uint (
      't',
      "two-coins",
      "TC",
      "will perform TC 2-coins payments, defaults to 1",
      &twocoins_number),
    GNUNET_GETOPT_option_uint (
      'U',
      "unaggregated-number",
      "UN",
      "will generate UN unaggregated payments, defaults to 1",
      &unaggregated_number),
    GNUNET_GETOPT_option_string (
      'u',
      "exchange-account-section",
      "SECTION",
      "use exchange bank account configuration from the given SECTION",
      &exchange_bank_section),
    GNUNET_GETOPT_OPTION_END
  };
  struct GNUNET_GETOPT_CommandLineOption ordinary_options[] = {
    GNUNET_GETOPT_option_string (
      'a',
      "apikey",
      "APIKEY",
      "HTTP 'Authorization' header to send to the merchant",
      &apikey),
    GNUNET_GETOPT_option_cfgfile (&cfg_filename),
    GNUNET_GETOPT_option_mandatory (
      GNUNET_GETOPT_option_string (
        'e',
        "exchange-account",
        "SECTION",
        "configuration section specifying the exchange account to use, mandatory",
        &exchange_account)),
    GNUNET_GETOPT_option_flag (
      'f',
      "fakebank",
      "use fakebank for the banking system",
      &use_fakebank),
    GNUNET_GETOPT_option_help (
      TALER_MERCHANT_project_data (),
      "Generate Taler ordinary payments"
      " to populate the databases"),
    GNUNET_GETOPT_option_string (
      'l',
      "logfile",
      "LF",
      "will log to file LF",
      &logfile),
    GNUNET_GETOPT_option_loglevel (&loglev),
    GNUNET_GETOPT_option_uint (
      'p',
      "payments-number",
      "PN",
      "will generate PN payments, defaults to 1",
      &payments_number),
    GNUNET_GETOPT_option_version (PACKAGE_VERSION "-" VCS_VERSION),
    GNUNET_GETOPT_OPTION_END
  };
  const char *default_config_file;

  default_config_file = TALER_MERCHANT_project_data ()->user_config_file;
  options = root_options;
  if (NULL != argv[1])
  {
    if (0 == strcmp ("ordinary", argv[1]))
    {
      ordinary = true;
      options = ordinary_options;
    }
    if (0 == strcmp ("corner", argv[1]))
    {
      corner = true;
      options = corner_options;
    }
  }

  {
    enum GNUNET_GenericReturnValue result;

    result = GNUNET_GETOPT_run ("taler-merchant-benchmark",
                                options,
                                argc,
                                argv);
    switch (result)
    {
    case GNUNET_SYSERR:
      return EXIT_INVALIDARGUMENT;
    case GNUNET_NO:
      return EXIT_SUCCESS;
    case GNUNET_OK:
      break;
    }
  }
  if (NULL == exchange_bank_section)
    exchange_bank_section = GNUNET_strdup ("exchange-account-1");
  if (NULL == loglev)
    loglev = GNUNET_strdup ("INFO");
  GNUNET_log_setup ("taler-merchant-benchmark",
                    loglev,
                    logfile);
  GNUNET_free (loglev);
  if ( (! ordinary) &&
       (! corner) )
  {
    TALER_LOG_ERROR ("Please use 'ordinary' or 'corner' subcommands.\n");
    return EXIT_INVALIDARGUMENT;
  }
  if (NULL == cfg_filename)
    cfg_filename = (char *) default_config_file;
  /* load currency from configuration */
  {
    struct GNUNET_CONFIGURATION_Handle *cfg;

    cfg = GNUNET_CONFIGURATION_create (TALER_MERCHANT_project_data ());
    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_load (cfg,
                                   cfg_filename))
    {
      TALER_LOG_ERROR ("Could not parse configuration\n");
      return EXIT_NOTCONFIGURED;
    }
    if (GNUNET_OK !=
        TALER_config_get_currency (cfg,
                                   "merchant",
                                   &currency))
    {
      TALER_LOG_ERROR ("Failed to read currency from configuration\n");
      GNUNET_CONFIGURATION_destroy (cfg);
      return EXIT_NOTCONFIGURED;
    }
    if (GNUNET_OK !=
        GNUNET_CONFIGURATION_get_value_string (cfg,
                                               "merchant-benchmark",
                                               "MERCHANT_URL",
                                               &merchant_url))
    {
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                                 "merchant-benchmark",
                                 "MERCHANT_URL");
      GNUNET_CONFIGURATION_destroy (cfg);
      return EXIT_NOTCONFIGURED;
    }
    if ( (0 == strlen (merchant_url)) ||
         (merchant_url[strlen (merchant_url) - 1] != '/') )
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 "merchant-benchmark",
                                 "MERCHANT_URL",
                                 "Not a valid URL");
      GNUNET_CONFIGURATION_destroy (cfg);
      return EXIT_NOTCONFIGURED;
    }

    if (GNUNET_OK !=
        TALER_TESTING_get_credentials (
          cfg_filename,
          exchange_bank_section,
          use_fakebank
          ? TALER_TESTING_BS_FAKEBANK
          : TALER_TESTING_BS_IBAN,
          &cred))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Required bank credentials not given in configuration\n");
      GNUNET_free (cfg_filename);
      return EXIT_NOTCONFIGURED;
    }

    GNUNET_CONFIGURATION_destroy (cfg);
  }
  GNUNET_asprintf (&alt_instance_url,
                   "%sinstances/%s/",
                   merchant_url,
                   alt_instance_id);
  {
    enum GNUNET_GenericReturnValue result;

    result = TALER_TESTING_loop (&run,
                                 NULL);
    return (GNUNET_OK == result)
      ? 0
      : EXIT_FAILURE;
  }
}
