/*
  This file is part of TALER
  Copyright (C) 2026 Taler Systems SA

  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.

  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 exchangedb/test_regressions.c
 * @brief regression tests for individual exchangedb operations
 *
 * Each check in the #tests table pins down one previously broken behaviour.
 * The tests share one scratch database, which test_regressions.sh creates and
 * removes; every check must therefore use fresh keys rather than assume an
 * empty database.
 */
#include "exchangedb_lib.h"
#include "taler/taler_json_lib.h"
#include "helper.h"
#include "exchange-database/create_tables.h"
#include "exchange-database/do_aggregate.h"
#include "exchange-database/compute_shard.h"
#include "exchange-database/start.h"
#include "exchange-database/commit.h"
#include "exchange-database/rollback.h"
#include "exchange-database/do_reserve_open.h"
#include "exchange-database/get_purse_deposit.h"
#include "exchange-database/get_reserve_close_info.h"
#include "exchange-database/begin_shard.h"
#include "exchange-database/abort_shard.h"
#include "exchange-database/update_shard_progress.h"
#include "exchange-database/do_import_credits.h"


/**
 * Currency we use, must match test_regressions.sh.
 */
#define CURRENCY "EUR"

/**
 * Report a failed expectation and return 1 from the calling check.
 */
#define FAILIF(cond)                            \
        do {                                          \
          if (! (cond)) break;                        \
          GNUNET_break (0);                           \
          fprintf (stderr,                            \
                   "FAILED: %s at %s:%u\n",           \
                   # cond, __FILE__, __LINE__);       \
          return 1;                                   \
        } while (0)


/**
 * Our database context.
 */
static struct TALER_EXCHANGEDB_PostgresContext *pg;

/**
 * Name of the single check to run, NULL to run all of them.
 */
static char *only;

/**
 * Return value of the process.
 */
static int result;


/**
 * Run @a sql on our database connection, outside of any transaction.
 *
 * @param sql statement(s) to run
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
exec_sql (const char *sql)
{
  struct GNUNET_PQ_ExecuteStatement es[] = {
    GNUNET_PQ_make_execute (sql),
    GNUNET_PQ_EXECUTE_STATEMENT_END
  };

  return GNUNET_PQ_exec_statements (pg->conn,
                                    es);
}


/**
 * Convert @a val to an amount in our currency.
 *
 * @param str amount without the currency prefix, e.g. "10.5"
 * @param[out] amount set to the parsed amount
 */
static void
parse_amount (const char *str,
              struct TALER_Amount *amount)
{
  char *s;

  GNUNET_asprintf (&s,
                   CURRENCY ":%s",
                   str);
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (s,
                                         amount));
  GNUNET_free (s);
}


/**
 * Build an INSERT that creates a reserve with a zero balance and no
 * `reserves_in` row, the way exchange_do_purse_merge() does.
 *
 * @param reserve_pub public key of the reserve to create
 * @return SQL statement, to be freed by the caller
 */
static char *
hex_insert_reserve (const struct TALER_ReservePublicKeyP *reserve_pub)
{
  char hex[sizeof (*reserve_pub) * 2 + 1];
  const unsigned char *raw = (const unsigned char *) reserve_pub;
  char *sql;

  for (unsigned int i = 0; i<sizeof (*reserve_pub); i++)
    GNUNET_snprintf (&hex[i * 2],
                     3,
                     "%02x",
                     raw[i]);
  GNUNET_asprintf (&sql,
                   "INSERT INTO reserves"
                   " (reserve_pub,current_balance,expiration_date,gc_date)"
                   " VALUES"
                   " (decode('%s','hex')"
                   " ,ROW(0,0)::taler_amount"
                   " ,1770000000000000"
                   " ,1780000000000000);",
                   hex);
  return sql;
}


/**
 * Render @a data as a lowercase hex string for use in an SQL literal.
 *
 * @param data binary data
 * @param size number of bytes in @a data
 * @return hex string, to be freed by the caller
 */
static char *
to_hex (const void *data,
        size_t size)
{
  const unsigned char *raw = data;
  char *hex;

  hex = GNUNET_malloc (size * 2 + 1);
  for (size_t i = 0; i<size; i++)
    GNUNET_snprintf (&hex[i * 2],
                     3,
                     "%02x",
                     raw[i]);
  return hex;
}


/**
 * E-2: a merchant may refund more than (deposit - deposit fee).  The
 * aggregator used to charge the full deposit fee on top of such a refund,
 * making the amount to be wired out negative, and then abort() on the
 * GNUNET_assert() guarding the subtraction -- stopping every payout for the
 * shard, and doing so again on every restart.
 *
 * Deposit EUR:1.00 of a denomination with a EUR:0.10 deposit fee, refund
 * EUR:0.95 of it, and require the aggregation to come out at EUR:0.
 */
static int
check_aggregate_refund_below_deposit_fee (void)
{
  struct TALER_MerchantPublicKeyP merchant_pub;
  struct TALER_FullPaytoHashP h_payto;
  struct TALER_NormalizedPaytoHashP h_norm;
  struct TALER_CoinSpendPublicKeyP coin_pub;
  struct TALER_WireTransferIdentifierRawP wtid;
  struct TALER_Amount total;
  struct TALER_Amount zero;
  char *sql;
  char *m_hex;
  char *p_hex;
  char *n_hex;
  char *c_hex;
  enum GNUNET_GenericReturnValue ok;

  parse_amount ("0",
                &zero);
  memset (&merchant_pub,
          0x41,
          sizeof (merchant_pub));
  memset (&h_payto,
          0x42,
          sizeof (h_payto));
  memset (&h_norm,
          0x43,
          sizeof (h_norm));
  memset (&coin_pub,
          0x44,
          sizeof (coin_pub));
  memset (&wtid,
          0x45,
          sizeof (wtid));
  m_hex = to_hex (&merchant_pub,
                  sizeof (merchant_pub));
  p_hex = to_hex (&h_payto,
                  sizeof (h_payto));
  n_hex = to_hex (&h_norm,
                  sizeof (h_norm));
  c_hex = to_hex (&coin_pub,
                  sizeof (coin_pub));
  GNUNET_asprintf (
    &sql,
    "INSERT INTO kyc_targets (h_normalized_payto)"
    " VALUES (decode('%s','hex')) ON CONFLICT DO NOTHING;"
    "INSERT INTO wire_targets"
    " (wire_target_h_payto,payto_uri,h_normalized_payto)"
    " VALUES (decode('%s','hex'),'payto://x-taler-bank/h/e2',"
    "         decode('%s','hex')) ON CONFLICT DO NOTHING;"
    "INSERT INTO denominations"
    " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig"
    " ,valid_from,expire_withdraw,expire_deposit,expire_legal"
    " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)"
    " VALUES (decode(repeat('e2',64),'hex'),1,0,decode('00','hex')"
    "        ,decode(repeat('00',64),'hex'),0,0,0,0"
    "        ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount"
    "        ,ROW(0,10000000)::taler_amount,ROW(0,0)::taler_amount"
    "        ,ROW(0,0)::taler_amount);"
    "INSERT INTO known_coins"
    " (denominations_serial,coin_pub,denom_sig,remaining)"
    " VALUES ((SELECT denominations_serial FROM denominations"
    "           WHERE denom_pub_hash=decode(repeat('e2',64),'hex'))"
    "        ,decode('%s','hex'),decode('00','hex'),ROW(0,0)::taler_amount);"
    "INSERT INTO batch_deposits"
    " (shard,merchant_pub,wallet_timestamp,exchange_timestamp"
    " ,refund_deadline,wire_deadline,h_contract_terms,wire_salt"
    " ,wire_target_h_payto,policy_blocked,total_amount,merchant_sig"
    " ,done,total_without_fee)"
    " VALUES (%llu,decode('%s','hex'),0,0,1,1"
    "        ,decode(repeat('e2',64),'hex'),decode(repeat('e2',16),'hex')"
    "        ,decode('%s','hex'),FALSE,ROW(1,0)::taler_amount"
    "        ,decode(repeat('00',64),'hex'),FALSE"
    "        ,ROW(0,90000000)::taler_amount);"
    "INSERT INTO coin_deposits"
    " (batch_deposit_serial_id,coin_pub,coin_sig,amount_with_fee)"
    " VALUES ((SELECT batch_deposit_serial_id FROM batch_deposits"
    "           WHERE merchant_pub=decode('%s','hex'))"
    "        ,decode('%s','hex'),decode(repeat('e2',64),'hex')"
    "        ,ROW(1,0)::taler_amount);"
    "INSERT INTO refunds"
    " (coin_pub,batch_deposit_serial_id,merchant_sig,rtransaction_id"
    " ,amount_with_fee)"
    " VALUES (decode('%s','hex')"
    "        ,(SELECT batch_deposit_serial_id FROM batch_deposits"
    "           WHERE merchant_pub=decode('%s','hex'))"
    "        ,decode(repeat('00',64),'hex'),1"
    "        ,ROW(0,95000000)::taler_amount);",
    n_hex,
    p_hex,
    n_hex,
    c_hex,
    (unsigned long long) TALER_EXCHANGEDB_compute_shard (&merchant_pub),
    m_hex,
    p_hex,
    m_hex,
    c_hex,
    c_hex,
    m_hex);
  ok = exec_sql (sql);
  GNUNET_free (sql);
  GNUNET_free (m_hex);
  GNUNET_free (p_hex);
  GNUNET_free (n_hex);
  GNUNET_free (c_hex);
  FAILIF (GNUNET_OK != ok);

  /* Before the fix this abort()ed inside TALER_EXCHANGEDB_do_aggregate(). */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_do_aggregate (pg,
                                         &h_payto,
                                         &merchant_pub,
                                         &wtid,
                                         &total));
  FAILIF (0 !=
          TALER_amount_cmp (&zero,
                            &total));
  return 0;
}


/**
 * E-13: PostgreSQL accepts COMMIT on a transaction it has already aborted,
 * rolls it back and answers with the command tag ROLLBACK and no error at
 * all.  TALER_EXCHANGEDB_commit() used to pass that straight through as
 * GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, i.e. every caller was told the
 * transaction had committed while its writes were gone.
 */
static int
check_commit_detects_rolled_back_transaction (void)
{
  /* A transaction that is aborted mid-way must NOT commit successfully. */
  FAILIF (GNUNET_OK !=
          TALER_EXCHANGEDB_start (pg,
                                  "test-e13-aborted"));
  FAILIF (GNUNET_OK !=
          exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)"
                    " VALUES (decode(repeat('13',32),'hex'));"));
  /* Provoke an error; from here on the transaction is doomed. */
  FAILIF (GNUNET_OK ==
          exec_sql ("SELECT 1/0;"));
  /* Before the fix this returned SUCCESS_NO_RESULTS (0). */
  FAILIF (0 <=
          TALER_EXCHANGEDB_commit (pg));
  /* ...and the row is indeed gone, so 'success' would have been a lie. */
  FAILIF (GNUNET_OK !=
          exec_sql ("DO $$ BEGIN"
                    " IF EXISTS (SELECT FROM kyc_targets"
                    "             WHERE h_normalized_payto"
                    "                   =decode(repeat('13',32),'hex'))"
                    " THEN RAISE EXCEPTION 'row survived a rollback';"
                    " END IF; END $$;"));

  /* A clean transaction must still commit and still return exactly 0. */
  FAILIF (GNUNET_OK !=
          TALER_EXCHANGEDB_start (pg,
                                  "test-e13-clean"));
  FAILIF (GNUNET_OK !=
          exec_sql ("INSERT INTO kyc_targets (h_normalized_payto)"
                    " VALUES (decode(repeat('14',32),'hex'));"));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
          TALER_EXCHANGEDB_commit (pg));
  FAILIF (GNUNET_OK !=
          exec_sql ("DO $$ BEGIN"
                    " IF NOT EXISTS (SELECT FROM kyc_targets"
                    "                 WHERE h_normalized_payto"
                    "                       =decode(repeat('14',32),'hex'))"
                    " THEN RAISE EXCEPTION 'committed row is missing';"
                    " END IF; END $$;"));
  return 0;
}


/**
 * Helper for the reserve-open checks: create a reserve with the given
 * balance and run exchange_do_reserve_open() on it.
 *
 * @param reserve_pub reserve to create and open
 * @param desired_expiration expiration the client asks for
 * @param now current time to assume
 * @param min_purse_limit number of purses the client asks for
 * @param open_fee annual account fee of the exchange
 * @param[out] open_cost set to the cost the exchange computed
 * @return transaction status
 */
static enum GNUNET_DB_QueryStatus
try_reserve_open (const struct TALER_ReservePublicKeyP *reserve_pub,
                  struct GNUNET_TIME_Timestamp desired_expiration,
                  struct GNUNET_TIME_Timestamp now,
                  uint32_t min_purse_limit,
                  const struct TALER_Amount *open_fee,
                  struct TALER_Amount *open_cost)
{
  struct TALER_ReserveSignatureP reserve_sig;
  struct TALER_Amount zero;
  struct TALER_Amount balance;
  struct GNUNET_TIME_Timestamp final_expiration;
  bool no_funds;
  char *sql;

  memset (&reserve_sig,
          0x51,
          sizeof (reserve_sig));
  parse_amount ("0",
                &zero);
  sql = hex_insert_reserve (reserve_pub);
  GNUNET_assert (GNUNET_OK ==
                 exec_sql (sql));
  GNUNET_free (sql);
  return TALER_EXCHANGEDB_do_reserve_open (pg,
                                           reserve_pub,
                                           &zero,
                                           &zero,
                                           min_purse_limit,
                                           &reserve_sig,
                                           desired_expiration,
                                           now,
                                           open_fee,
                                           &no_funds,
                                           &balance,
                                           open_cost,
                                           &final_expiration);
}


/**
 * E-3: `{"reserve_expiration":"never"}` is a perfectly well-formed request:
 * GNUNET_JSON_spec_timestamp() turns it into GNUNET_TIME_UNIT_FOREVER_ABS and
 * qconv_abs_time() clamps that to INT64_MAX.  The stored procedure then
 * overflowed INT8 computing the number of years, which SQLSTATE 22003 turns
 * into a hard error and the handler into an HTTP 500.
 */
static int
check_reserve_open_never_expires (void)
{
  struct TALER_ReservePublicKeyP reserve_pub;
  struct TALER_Amount open_fee;
  struct TALER_Amount open_cost;

  memset (&reserve_pub,
          0x33,
          sizeof (reserve_pub));
  /* No fractional part here; that is E-4's business. */
  parse_amount ("1",
                &open_fee);
  /* Before the fix: 'bigint out of range' -> HARD_ERROR. */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          try_reserve_open (&reserve_pub,
                            GNUNET_TIME_UNIT_FOREVER_TS,
                            GNUNET_TIME_timestamp_get (),
                            1,
                            &open_fee,
                            &open_cost));
  return 0;
}


/**
 * E-4: `my_years * in_open_fee.frac` was an INT4 multiplication that
 * overflowed *before* the division, so the author's own overflow guard could
 * never be reached: an exchange whose ACCOUNT_FEE has a fractional part
 * answered a far-future reserve-open request with an HTTP 500.
 *
 * The same block computed the new purse limit in INT4 from the client's
 * `purse_limit`, which overflows for any value near INT32_MAX.
 */
static int
check_reserve_open_int4_overflows (void)
{
  struct TALER_ReservePublicKeyP reserve_pub;
  struct TALER_Amount open_fee;
  struct TALER_Amount open_cost;
  struct TALER_Amount expected;
  struct GNUNET_TIME_Timestamp now;

  parse_amount ("0.5",
                &open_fee);
  parse_amount ("25",
                &expected);

  /* 50 years at EUR:0.50/year: 50 * 50000000 does not fit into an INT4. */
  memset (&reserve_pub,
          0x34,
          sizeof (reserve_pub));
  now = GNUNET_TIME_timestamp_get ();
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          try_reserve_open (&reserve_pub,
                            GNUNET_TIME_absolute_to_timestamp (
                              GNUNET_TIME_absolute_add (
                                now.abs_time,
                                GNUNET_TIME_relative_multiply (
                                  GNUNET_TIME_UNIT_YEARS,
                                  50))),
                            now,
                            1,
                            &open_fee,
                            &open_cost));
  FAILIF (0 !=
          TALER_amount_cmp (&expected,
                            &open_cost));

  /* An absurd purse_limit must be priced out of range, not overflow. */
  memset (&reserve_pub,
          0x35,
          sizeof (reserve_pub));
  now = GNUNET_TIME_timestamp_get ();
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          try_reserve_open (&reserve_pub,
                            now,
                            now,
                            2147483647,
                            &open_fee,
                            &open_cost));
  return 0;
}


/**
 * E-5: `known_coins.age_commitment_hash` is NULL for every coin without an
 * age commitment -- the common case -- but the result spec read it without
 * GNUNET_PQ_result_spec_allow_null(), so building the 409 conflict proof for
 * POST /purses/$PURSE_PUB/deposit failed with an HTTP 500 instead.
 */
static int
check_purse_deposit_without_age_commitment (void)
{
  struct TALER_PurseContractPublicKeyP purse_pub;
  struct TALER_CoinSpendPublicKeyP coin_pub;
  struct TALER_Amount amount;
  struct TALER_DenominationHashP h_denom_pub;
  struct TALER_AgeCommitmentHashP hac;
  struct TALER_CoinSpendSignatureP coin_sig;
  bool no_age_commitment;
  char *partner_url = NULL;
  char *sql;
  char *p_hex;
  char *c_hex;
  enum GNUNET_GenericReturnValue ok;

  memset (&purse_pub,
          0x55,
          sizeof (purse_pub));
  memset (&coin_pub,
          0x56,
          sizeof (coin_pub));
  p_hex = to_hex (&purse_pub,
                  sizeof (purse_pub));
  c_hex = to_hex (&coin_pub,
                  sizeof (coin_pub));
  GNUNET_asprintf (
    &sql,
    "INSERT INTO denominations"
    " (denom_pub_hash,denom_type,age_mask,denom_pub,master_sig"
    " ,valid_from,expire_withdraw,expire_deposit,expire_legal"
    " ,coin,fee_withdraw,fee_deposit,fee_refresh,fee_refund)"
    " VALUES (decode(repeat('e5',64),'hex'),1,0,decode('00','hex')"
    "        ,decode(repeat('00',64),'hex'),0,0,0,0"
    "        ,ROW(1,0)::taler_amount,ROW(0,0)::taler_amount"
    "        ,ROW(0,0)::taler_amount,ROW(0,0)::taler_amount"
    "        ,ROW(0,0)::taler_amount);"
    /* age_commitment_hash deliberately left NULL */
    "INSERT INTO known_coins"
    " (denominations_serial,coin_pub,denom_sig,remaining)"
    " VALUES ((SELECT denominations_serial FROM denominations"
    "           WHERE denom_pub_hash=decode(repeat('e5',64),'hex'))"
    "        ,decode('%s','hex'),decode('00','hex')"
    "        ,ROW(0,0)::taler_amount);"
    "INSERT INTO purse_deposits"
    " (purse_pub,coin_pub,amount_with_fee,coin_sig)"
    " VALUES (decode('%s','hex'),decode('%s','hex')"
    "        ,ROW(1,0)::taler_amount,decode(repeat('e5',64),'hex'));",
    c_hex,
    p_hex,
    c_hex);
  ok = exec_sql (sql);
  GNUNET_free (sql);
  GNUNET_free (p_hex);
  GNUNET_free (c_hex);
  FAILIF (GNUNET_OK != ok);

  /* Before the fix: HARD_ERROR from the failed NULL extraction. */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_get_purse_deposit (pg,
                                              &purse_pub,
                                              &coin_pub,
                                              &amount,
                                              &h_denom_pub,
                                              &hac,
                                              &no_age_commitment,
                                              &coin_sig,
                                              &partner_url));
  GNUNET_free (partner_url);
  FAILIF (! no_age_commitment);
  return 0;
}


/**
 * E-6: a reserve funded by a purse merge has no `reserves_in` row, so the
 * LEFT JOIN in get_reserve_close_info() returns a NULL payto_uri.  Without
 * allow_null the extraction failed and POST /reserves/$RP/close answered 500
 * instead of the 409 the handler already implements.
 */
static int
check_reserve_close_info_without_origin (void)
{
  struct TALER_ReservePublicKeyP reserve_pub;
  struct TALER_Amount balance;
  struct TALER_FullPayto payto_uri;
  char *sql;
  enum GNUNET_GenericReturnValue ok;

  memset (&reserve_pub,
          0x36,
          sizeof (reserve_pub));
  sql = hex_insert_reserve (&reserve_pub);
  ok = exec_sql (sql);
  GNUNET_free (sql);
  FAILIF (GNUNET_OK != ok);

  /* Before the fix: HARD_ERROR from the failed NULL extraction. */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_get_reserve_close_info (pg,
                                                   &reserve_pub,
                                                   &balance,
                                                   &payto_uri));
  FAILIF (NULL != payto_uri.full_payto);
  return 0;
}


/**
 * Read back the bookkeeping of one work shard.
 *
 * @param job_name job the shard belongs to
 * @param start_row inclusive start row of the shard
 * @param end_row exclusive end row of the shard
 * @param[out] progress_row how far the shard has come
 * @param[out] completed whether the shard is done
 * @return transaction status code
 */
static enum GNUNET_DB_QueryStatus
get_shard_state (const char *job_name,
                 uint64_t start_row,
                 uint64_t end_row,
                 uint64_t *progress_row,
                 bool *completed)
{
  struct GNUNET_PQ_QueryParam params[] = {
    GNUNET_PQ_query_param_string (job_name),
    GNUNET_PQ_query_param_uint64 (&start_row),
    GNUNET_PQ_query_param_uint64 (&end_row),
    GNUNET_PQ_query_param_end
  };
  struct GNUNET_PQ_ResultSpec rs[] = {
    GNUNET_PQ_result_spec_uint64 ("progress_row",
                                  progress_row),
    GNUNET_PQ_result_spec_bool ("completed",
                                completed),
    GNUNET_PQ_result_spec_end
  };

  PREPARE (pg,
           "test_get_shard_state",
           "SELECT"
           " progress_row"
           ",completed"
           " FROM work_shards"
           " WHERE job_name=$1"
           "   AND start_row=$2"
           "   AND end_row=$3;");
  return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
                                                   "test_get_shard_state",
                                                   params,
                                                   rs);
}


/**
 * A worker that gets part of the way through a shard and then stops used to
 * leave nothing behind: the shard was either untouched or completed, so the
 * next worker to pick it up redid all of it, and nothing it had imported was
 * visible until the whole shard was done.  Check that the progress marker is
 * kept, that it survives releasing the shard, that it never moves backwards,
 * and that reaching the end of the shard is what completes it.
 */
static int
check_shard_progress_survives_abort (void)
{
  const char *job = "test-shard-progress";
  uint64_t start;
  uint64_t end;
  uint64_t progress;
  uint64_t start2;
  uint64_t end2;
  uint64_t progress2;
  bool completed;

  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_begin_shard (pg,
                                        job,
                                        GNUNET_TIME_UNIT_HOURS,
                                        1024,
                                        &start,
                                        &end,
                                        &progress));
  /* A fresh shard has nothing done yet. */
  FAILIF (progress != start);

  /* Get half way, then let go of the shard. */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_update_shard_progress (pg,
                                                  job,
                                                  start,
                                                  end,
                                                  start + 512,
                                                  GNUNET_TIME_UNIT_HOURS));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_abort_shard (pg,
                                        job,
                                        start,
                                        end));

  /* The next worker gets the same shard back, but resumes in the middle. */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_begin_shard (pg,
                                        job,
                                        GNUNET_TIME_UNIT_HOURS,
                                        1024,
                                        &start2,
                                        &end2,
                                        &progress2));
  FAILIF (start2 != start);
  FAILIF (end2 != end);
  FAILIF (progress2 != start + 512);

  /* A straggler reporting older progress must not rewind the shard. */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_update_shard_progress (pg,
                                                  job,
                                                  start,
                                                  end,
                                                  start + 1,
                                                  GNUNET_TIME_UNIT_HOURS));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          get_shard_state (job,
                           start,
                           end,
                           &progress,
                           &completed));
  FAILIF (progress != start + 512);
  FAILIF (completed);

  /* Reaching the end completes the shard; there is no second statement for
     the caller to forget, or to crash before. */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_update_shard_progress (pg,
                                                  job,
                                                  start,
                                                  end,
                                                  end,
                                                  GNUNET_TIME_UNIT_HOURS));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          get_shard_state (job,
                           start,
                           end,
                           &progress,
                           &completed));
  FAILIF (progress != end);
  FAILIF (! completed);
  return 0;
}


/**
 * Importing a batch of incoming wire transfers and recording how far the
 * shard has come is one statement, so a crash cannot land one without the
 * other.  Check that a batch moves both, that importing it again is harmless
 * and reported as a duplicate, and that the shard completes as part of the
 * import rather than in a transaction of its own.
 */
static int
check_import_credits_advances_shard (void)
{
  const char *job = "test-import-credits";
  struct TALER_ReservePublicKeyP reserve_pub;
  struct TALER_Amount balance;
  struct TALER_EXCHANGEDB_ReserveInInfo reserve;
  struct TALER_EXCHANGEDB_CreditBatch batch;
  enum GNUNET_DB_QueryStatus results[1];
  uint64_t start;
  uint64_t end;
  uint64_t progress;
  bool completed;

  memset (&reserve_pub,
          0x51,
          sizeof (reserve_pub));
  parse_amount ("4.00",
                &balance);
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_EXCHANGEDB_begin_shard (pg,
                                        job,
                                        GNUNET_TIME_UNIT_HOURS,
                                        1024,
                                        &start,
                                        &end,
                                        &progress));
  reserve.reserve_pub = &reserve_pub;
  reserve.balance = &balance;
  reserve.execution_time = GNUNET_TIME_timestamp_get ();
  reserve.sender_account_details.full_payto
    = (char *) "payto://x-taler-bank/localhost/shard-test?receiver-name=Shard";
  reserve.wire_reference = start + 1;
  memset (&batch,
          0,
          sizeof (batch));
  batch.exchange_account_name = "exchange-account-test";
  batch.reserves = &reserve;
  batch.reserves_length = 1;
  batch.job_name = job;
  batch.shard_start = start;
  batch.shard_end = end;
  batch.progress_row = start + 1;
  batch.lease = GNUNET_TIME_UNIT_HOURS;

  FAILIF (0 >
          TALER_EXCHANGEDB_do_import_credits (pg,
                                              &batch,
                                              results));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != results[0]);
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          get_shard_state (job,
                           start,
                           end,
                           &progress,
                           &completed));
  /* The transfer is committed and so is the fact that we consumed its row --
     long before the rest of the shard has been looked at. */
  FAILIF (progress != start + 1);
  FAILIF (completed);

  /* Re-importing the same batch is what happens whenever a shard is picked up
     twice.  It has to be harmless. */
  batch.progress_row = end;
  FAILIF (0 >
          TALER_EXCHANGEDB_do_import_credits (pg,
                                              &batch,
                                              results));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != results[0]);
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          get_shard_state (job,
                           start,
                           end,
                           &progress,
                           &completed));
  FAILIF (progress != end);
  FAILIF (! completed);
  return 0;
}


/**
 * All checks we know about.
 */
static const struct
{
  const char *name;
  int (*fn)(void);
} tests[] = {
  { "aggregate-refund-below-deposit-fee",
    &check_aggregate_refund_below_deposit_fee },
  { "commit-detects-rolled-back-transaction",
    &check_commit_detects_rolled_back_transaction },
  { "reserve-open-never-expires",
    &check_reserve_open_never_expires },
  { "reserve-open-int4-overflows",
    &check_reserve_open_int4_overflows },
  { "purse-deposit-without-age-commitment",
    &check_purse_deposit_without_age_commitment },
  { "reserve-close-info-without-origin",
    &check_reserve_close_info_without_origin },
  { "shard-progress-survives-abort",
    &check_shard_progress_survives_abort },
  { "import-credits-advances-shard",
    &check_import_credits_advances_shard },
  { NULL, NULL }
};


/**
 * Main function that runs the checks.
 *
 * @param cls closure
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used
 * @param cfg configuration
 */
static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  unsigned int ran = 0;

  (void) cls;
  (void) args;
  (void) cfgfile;
  pg = TALER_EXCHANGEDB_connect_admin (cfg);
  if (NULL == pg)
  {
    fprintf (stderr,
             "Failed to connect to the database\n");
    result = 77;
    return;
  }
  if (GNUNET_OK !=
      TALER_EXCHANGEDB_create_tables (pg,
                                      false,
                                      0))
  {
    fprintf (stderr,
             "Failed to create the database schema\n");
    result = 77;
    goto cleanup;
  }
  for (unsigned int i = 0; NULL != tests[i].name; i++)
  {
    if ( (NULL != only) &&
         (0 != strcmp (only,
                       tests[i].name)) )
      continue;
    fprintf (stderr,
             "Running check `%s'\n",
             tests[i].name);
    ran++;
    if (0 != tests[i].fn ())
    {
      fprintf (stderr,
               "Check `%s' FAILED\n",
               tests[i].name);
      result = 1;
    }
  }
  if (0 == ran)
  {
    fprintf (stderr,
             "No check matched `%s'\n",
             only);
    result = 1;
  }
cleanup:
  TALER_EXCHANGEDB_disconnect (pg);
  pg = NULL;
}


int
main (int argc,
      char *const *argv)
{
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_string ('t',
                                 "test",
                                 "NAME",
                                 "only run the check called NAME",
                                 &only),
    GNUNET_GETOPT_OPTION_END
  };
  enum GNUNET_GenericReturnValue ret;

  result = 0;
  ret = GNUNET_PROGRAM_run (TALER_EXCHANGE_project_data (),
                            argc,
                            argv,
                            "test-regressions",
                            "Regression tests for the exchange database layer",
                            options,
                            &run,
                            NULL);
  if (GNUNET_SYSERR == ret)
    return 3;
  if (GNUNET_NO == ret)
    return 0;
  return result;
}


/* end of test_regressions.c */
