/*
  This file is part of TALER
  Copyright (C) 2024 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 src/donaudb/test_donaudb.c
 * @brief test cases for DB interaction functions
 * @author Johannes Casaburi
 */
#include "donau_config.h"
#include <taler/taler_json_lib.h>
#include "donaudb_lib.h"
#include "donau_util.h"
#include "helper.h"
#include "donaudb_lib.h"
#include "donau-database/commit.h"
#include "donau-database/create_tables.h"
#include "donau-database/delete_charity.h"
#include "donau-database/drop_tables.h"
#include "donau-database/event_listen_cancel.h"
#include "donau-database/event_listen.h"
#include "donau-database/event_notify.h"
#include "donau-database/iterate_charities.h"
#include "donau-database/iterate_history_entries.h"
#include "donau-database/insert_charity.h"
#include "donau-database/insert_donation_unit.h"
#include "donau-database/insert_history_entry.h"
#include "donau-database/do_insert_receipt_issued.h"
#include "donau-database/insert_signkey.h"
#include "donau-database/insert_receipts_submitted.h"
#include "donau-database/iterate_active_signkeys.h"
#include "donau-database/iterate_donation_units.h"
#include "donau-database/get_receipts_submitted_total.h"
#include "donau-database/get_charity.h"
#include "donau-database/get_donation_unit_amount.h"
#include "donau-database/get_receipt_issued.h"
#include "donau-database/get_signkey.h"
#include "donau-database/preflight.h"
#include "donau-database/rollback.h"
#include "donau-database/start.h"
#include "donau-database/start_read_committed.h"
#include "donau-database/start_read_only.h"
#include "donau-database/update_charity.h"

/**
 * Global result from the testcase.
 */
static int result;

/**
 * Report line of error if @a cond is true, and jump to label "drop".
 */
#define FAILIF(cond)                              \
        do {                                          \
          if (! (cond)) { break;}                      \
          GNUNET_break (0);                           \
          goto drop;                                  \
        } while (0)


/**
 * Initializes @a ptr with random data.
 */
#define RND_BLK(ptr)                                                    \
        GNUNET_CRYPTO_random_block (ptr, sizeof (* \
                                                 ptr))

/**
 * Initializes @a ptr with zeros.
 */
#define ZR_BLK(ptr) \
        memset (ptr, 0, sizeof (*ptr))

/**
 * How big do we make the RSA keys?
 */
#define RSA_KEY_SIZE 1024

/**
 * Currency we use.  Must match test-donau-db-*.conf.
 */
#define CURRENCY "EUR"

/**
 * Database plugin under test.
 */
static struct DONAUDB_PostgresContext *ctx;

/**
 * Denomination key pair used to manufacture blinded donation unit
 * signatures for the regression tests below.  Created on demand.
 */
static struct TALER_DenominationPrivateKey test_denom_priv;

/**
 * Public key matching #test_denom_priv.
 */
static struct TALER_DenominationPublicKey test_denom_pub;


/**
 * Create a blinded donation unit signature usable as filler data
 * for `receipts_issued.blinded_sig'.
 *
 * @return freshly allocated blinded signature
 */
static struct GNUNET_CRYPTO_BlindedSignature *
make_blinded_sig (void)
{
  struct GNUNET_CRYPTO_BlindedMessage *rp;
  struct GNUNET_CRYPTO_BlindedSignature *bs;

  if (NULL == test_denom_priv.bsign_priv_key)
    GNUNET_assert (GNUNET_OK ==
                   TALER_denom_priv_create (&test_denom_priv,
                                            &test_denom_pub,
                                            GNUNET_CRYPTO_BSA_RSA,
                                            RSA_KEY_SIZE));
  rp = GNUNET_new (struct GNUNET_CRYPTO_BlindedMessage);
  rp->cipher = GNUNET_CRYPTO_BSA_RSA;
  rp->rc = 1;
  rp->details.rsa_blinded_message.blinded_msg_size = 32;
  rp->details.rsa_blinded_message.blinded_msg = GNUNET_malloc (32);
  GNUNET_CRYPTO_random_block (rp->details.rsa_blinded_message.blinded_msg,
                              32);
  bs = GNUNET_CRYPTO_blind_sign (test_denom_priv.bsign_priv_key,
                                 "rw",
                                 rp);
  GNUNET_assert (NULL != bs);
  GNUNET_CRYPTO_blinded_message_decref (rp);
  return bs;
}


/**
 * Register a fresh charity with a random public key.
 *
 * @param max_per_year annual donation limit to use
 * @param[out] charity_id set to the ID of the new charity
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
make_charity (const char *max_per_year,
              uint64_t *charity_id)
{
  struct DONAU_CharityPublicKeyP charity_pub;
  struct TALER_Amount max;

  RND_BLK (&charity_pub);
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (max_per_year,
                                         &max));
  return (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
          DONAUDB_insert_charity (ctx,
                                  &charity_pub,
                                  "test charity",
                                  "https://charity.example.com/",
                                  &max,
                                  charity_id))
         ? GNUNET_OK
         : GNUNET_SYSERR;
}


/**
 * Issue one (fresh, never seen before) receipt for @a charity_id.
 *
 * @param charity_id charity to issue for
 * @param year year to attribute the receipt to
 * @param amount amount of the receipt
 * @param[out] under_limit set to true if the charity stayed below its limit
 * @return database status of the operation
 */
static enum GNUNET_DB_QueryStatus
issue_receipt (uint64_t charity_id,
               uint32_t year,
               const char *amount,
               bool *under_limit)
{
  struct DONAU_BlindedDonationUnitSignature du_sigs[1];
  struct DONAU_DonationReceiptHashP h_receipt;
  struct TALER_Amount amt;
  bool charity_unknown;
  enum GNUNET_DB_QueryStatus qs;

  RND_BLK (&h_receipt);
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (amount,
                                         &amt));
  du_sigs[0].blinded_sig = make_blinded_sig ();
  qs = DONAUDB_do_insert_receipt_issued (ctx,
                                         year,
                                         1,
                                         du_sigs,
                                         charity_id,
                                         &h_receipt,
                                         &amt,
                                         under_limit,
                                         &charity_unknown);
  GNUNET_CRYPTO_blinded_sig_decref (du_sigs[0].blinded_sig);
  return qs;
}


/**
 * Return charities information.
 *
 * @param cls closure
 */
static enum GNUNET_GenericReturnValue
charities_cb (
  void *cls,
  uint64_t charity_id,
  const struct DONAU_CharityPublicKeyP *charity_pub,
  const char *charity_name,
  const struct TALER_Amount *max_per_year,
  uint32_t current_year,
  const struct TALER_Amount *receipts_to_date)
{
  (void) cls;
  (void) charity_id;
  (void) charity_name;
  (void) max_per_year;
  (void) current_year;
  (void) receipts_to_date;
  return GNUNET_OK;
}


/**
 * Function called with information about the donau's donation_unit keys.
 *
 * @param cls NULL
 * @param donation_unit_pub public key of the donation_unit
 * @param h_donation_unit_pub hash of @a donation_unit_pub
 * @param validity_year of the donation unit
 * @param value of the donation unit
 */
static enum GNUNET_GenericReturnValue
donation_unit_info_cb (
  void *cls,
  const struct DONAU_DonationUnitHashP *h_donation_unit_pub,
  const struct DONAU_DonationUnitPublicKey *donation_unit_pub,
  uint64_t validity_year,
  struct TALER_Amount *value)
{
  (void) cls;
  (void) h_donation_unit_pub;
  (void) donation_unit_pub;
  (void) validity_year;
  (void) value;
  return GNUNET_OK;
}


/**
 * Create an unblinded donation unit signature.  The database layer only
 * serializes it, so any well-formed signature will do.
 *
 * @return freshly allocated unblinded signature
 */
static struct GNUNET_CRYPTO_UnblindedSignature *
make_unblinded_sig (void)
{
  struct GNUNET_CRYPTO_UnblindedSignature *us;
  struct GNUNET_HashCode msg;

  if (NULL == test_denom_priv.bsign_priv_key)
    GNUNET_assert (GNUNET_OK ==
                   TALER_denom_priv_create (&test_denom_priv,
                                            &test_denom_pub,
                                            GNUNET_CRYPTO_BSA_RSA,
                                            RSA_KEY_SIZE));
  RND_BLK (&msg);
  us = GNUNET_new (struct GNUNET_CRYPTO_UnblindedSignature);
  us->cipher = GNUNET_CRYPTO_BSA_RSA;
  us->rc = 1;
  us->details.rsa_signature
    = GNUNET_CRYPTO_rsa_sign_fdh (
        test_denom_priv.bsign_priv_key->details.rsa_private_key,
        &msg,
        sizeof (msg));
  GNUNET_assert (NULL != us->details.rsa_signature);
  return us;
}


/**
 * Register a donation unit with a random key hash.
 *
 * @param[out] h_donation_unit_pub set to the hash identifying the unit
 * @param year validity year of the unit
 * @param value value of the unit
 * @return database status of the operation
 */
static enum GNUNET_DB_QueryStatus
make_donation_unit (struct DONAU_DonationUnitHashP *h_donation_unit_pub,
                    uint64_t year,
                    const char *value)
{
  struct TALER_DenominationPrivateKey dpriv;
  struct TALER_DenominationPublicKey dpub;
  struct DONAU_DonationUnitPublicKey du_pub;
  struct TALER_Amount val;
  enum GNUNET_DB_QueryStatus qs;

  RND_BLK (h_donation_unit_pub);
  GNUNET_assert (GNUNET_OK ==
                 TALER_denom_priv_create (&dpriv,
                                          &dpub,
                                          GNUNET_CRYPTO_BSA_RSA,
                                          RSA_KEY_SIZE));
  du_pub.bsign_pub_key = dpub.bsign_pub_key;
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (value,
                                         &val));
  qs = DONAUDB_insert_donation_unit (ctx,
                                     h_donation_unit_pub,
                                     &du_pub,
                                     year,
                                     &val);
  TALER_denom_priv_free (&dpriv);
  TALER_denom_pub_free (&dpub);
  return qs;
}


/**
 * Function called with information about the donau's online signing keys.
 *
 * @param cls NULL
 * @param donau_pub the public key
 * @param meta meta data information about the denomination type (expirations)
 */
static void
iterate_active_signkeys_cb (
  void *cls,
  const struct DONAU_DonauPublicKeyP *donau_pub,
  struct DONAUDB_SignkeyMetaData *meta)
{
  (void) cls;
  (void) donau_pub;
  (void) meta;
}


/**
 * Closure for #check_charity_cb().
 */
struct CharityCheck
{
  /**
   * Charity we are interested in.
   */
  uint64_t charity_id;

  /**
   * Value #DONAUDB_iterate_charities() reported for it.
   */
  struct TALER_Amount receipts_to_date;

  /**
   * Set to true if the charity was seen.
   */
  bool found;
};


/**
 * Capture what #DONAUDB_iterate_charities() reports for one charity.
 *
 * @param cls a `struct CharityCheck *`
 * @param charity_id charity of this row
 * @param charity_pub public key of the charity
 * @param charity_name name of the charity
 * @param max_per_year annual limit
 * @param current_year year `receipts_to_date' belongs to
 * @param receipts_to_date total booked so far
 * @return #GNUNET_OK to continue
 */
static enum GNUNET_GenericReturnValue
check_charity_cb (void *cls,
                  uint64_t charity_id,
                  const struct DONAU_CharityPublicKeyP *charity_pub,
                  const char *charity_name,
                  const struct TALER_Amount *max_per_year,
                  uint32_t current_year,
                  const struct TALER_Amount *receipts_to_date)
{
  struct CharityCheck *cc = cls;

  (void) charity_pub;
  (void) charity_name;
  (void) max_per_year;
  (void) current_year;
  if (charity_id != cc->charity_id)
    return GNUNET_OK;
  cc->receipts_to_date = *receipts_to_date;
  cc->found = true;
  return GNUNET_OK;
}


/**
 * Closure for #collect_history_cb().
 */
struct HistoryCheck
{
  /**
   * Charity we are interested in.
   */
  unsigned long long charity_id;

  /**
   * Year we expect a history entry for.
   */
  uint64_t donation_year;

  /**
   * Amount we expect that entry to carry.
   */
  struct TALER_Amount expected;

  /**
   * Set to true if the expected entry was found.
   */
  bool found;
};


/**
 * Look for the history entry described by @a cls.
 *
 * @param cls a `struct HistoryCheck *`
 * @param charity_id charity of this entry
 * @param final_amount closing total of that year
 * @param donation_year year of this entry
 * @return #GNUNET_OK to continue
 */
static enum GNUNET_GenericReturnValue
collect_history_cb (void *cls,
                    unsigned long long charity_id,
                    struct TALER_Amount final_amount,
                    uint64_t donation_year)
{
  struct HistoryCheck *hc = cls;

  if ( (charity_id == hc->charity_id) &&
       (donation_year == hc->donation_year) &&
       (0 == TALER_amount_cmp (&final_amount,
                               &hc->expected)) )
    hc->found = true;
  return GNUNET_OK;
}


/**
 * Main function that will be run by the scheduler.
 *
 * @param cls closure with config
 */
static void
run (void *cls)
{
  struct GNUNET_CONFIGURATION_Handle *cfg = cls;
  unsigned int year;

  // Charity information
  json_t *charities;
  struct DONAU_CharityPublicKeyP charity_pub;
  struct DONAUDB_CharityMetaData charity_meta;
  const char *charity_name;
  const char *charity_url;
  struct TALER_Amount max_per_year;
  struct TALER_Amount receipts_to_date;
  uint64_t charity_id;

  // Donation unit information
  struct DONAU_DonationUnitHashP h_donation_unit_pub;
  uint64_t validity_year;
  struct TALER_Amount du_value;

  // Signing key information
  struct DONAU_DonauPublicKeyP donau_pub;
  struct DONAUDB_SignkeyMetaData sk_meta;

  // Issued receipts information
  size_t num_b_sigs = 1;
  struct DONAU_BlindedDonationUnitSignature du_sigs[num_b_sigs];
  struct DONAU_DonationReceiptHashP h_receipt;
  struct TALER_Amount amount_receipts;
  bool smaller_than_max_per_year;
  bool charity_unknown;
  bool charity_in_use;
  struct TALER_DenominationPrivateKey denom_priv;
  struct TALER_DenominationPublicKey denom_pub;
  struct DONAU_DonationUnitPublicKey du_pub;
  struct GNUNET_CRYPTO_BlindedMessage *rp;
  struct GNUNET_CRYPTO_RsaBlindedMessage *rsa;

  if (NULL ==
      (ctx = DONAUDB_connect_admin (cfg)))
  {
    fprintf (stderr,
             "Failed to connect to database\n");
    result = 77;
    return;
  }
  (void) DONAUDB_drop_tables (ctx);
  if (GNUNET_OK !=
      DONAUDB_create_tables (ctx))
  {
    fprintf (stderr,
             "Failed to create DB tables\n");
    result = 77;
    goto cleanup;
  }
  DONAUDB_preflight (ctx);
  FAILIF (GNUNET_OK !=
          DONAUDB_start (ctx,
                         "test-1"));

  fprintf (stderr,
           "Running DB tests\n");

  /* test DB is empty */
  charity_id = 1;
  FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
          DONAUDB_get_charity (ctx,
                               charity_id,
                               &charity_meta));

  /* test insert charity */
  charity_name = "charity_name";
  charity_url = "charity_url";
  charities = json_array ();
  RND_BLK (&charity_pub);
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (CURRENCY ":1.000010",
                                         &max_per_year));
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (CURRENCY ":0.000010",
                                         &receipts_to_date));

  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          DONAUDB_insert_charity (ctx,
                                  &charity_pub,
                                  charity_name,
                                  charity_url,
                                  &max_per_year,
                                  &charity_id));

  /* test get charities */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          DONAUDB_iterate_charities (ctx,
                                     &charities_cb,
                                     charities));

  {
    /* Update the charity and verify the new key and metadata persist. */
    const char *updated_charity_name = "charity_name_updated";
    const char *updated_charity_url = "charity_url_updated";
    struct TALER_Amount updated_max;
    struct DONAU_CharityPrivateKeyP updated_charity_priv;
    struct DONAU_CharityPublicKeyP updated_charity_pub;
    bool conflict = true;

    GNUNET_CRYPTO_eddsa_key_create (&updated_charity_priv.eddsa_priv);
    GNUNET_CRYPTO_eddsa_key_get_public (&updated_charity_priv.eddsa_priv,
                                        &updated_charity_pub.eddsa_pub);

    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (CURRENCY ":2.000000",
                                           &updated_max));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_update_charity (ctx,
                                    charity_id,
                                    &updated_charity_pub,
                                    updated_charity_name,
                                    updated_charity_url,
                                    &updated_max,
                                    &conflict));
    FAILIF (conflict);

    ZR_BLK (&charity_meta);
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_get_charity (ctx,
                                 charity_id,
                                 &charity_meta));
    GNUNET_assert (0 == GNUNET_memcmp (&charity_meta.charity_pub,
                                       &updated_charity_pub));
    GNUNET_assert (0 == strcmp (charity_meta.charity_name,
                                updated_charity_name));
    GNUNET_assert (0 == strcmp (charity_meta.charity_url,
                                updated_charity_url));
    GNUNET_assert (0 == TALER_amount_cmp (&charity_meta.max_per_year,
                                          &updated_max));
    GNUNET_free (charity_meta.charity_name);
    GNUNET_free (charity_meta.charity_url);

    charity_name = updated_charity_name;
    charity_url = updated_charity_url;
    max_per_year = updated_max;
    charity_pub = updated_charity_pub;
  }

  /* test delete charity */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          DONAUDB_delete_charity (ctx,
                                  charity_id,
                                  &charity_in_use));

  /* test insert donation unit */
  RND_BLK (&h_donation_unit_pub);
  GNUNET_assert (GNUNET_OK ==
                 TALER_denom_priv_create (&denom_priv,
                                          &denom_pub,
                                          GNUNET_CRYPTO_BSA_RSA,
                                          RSA_KEY_SIZE));
  du_pub.bsign_pub_key = denom_pub.bsign_pub_key;
  validity_year = 2024;
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (CURRENCY ":1.000010",
                                         &du_value));

  /* test iterate donation units */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
          DONAUDB_iterate_donation_units (ctx,
                                          &donation_unit_info_cb,
                                          NULL));

  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          DONAUDB_insert_donation_unit (ctx,
                                        &h_donation_unit_pub,
                                        &du_pub,
                                        validity_year,
                                        &du_value));

  /* test iterate donation units */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          DONAUDB_iterate_donation_units (ctx,
                                          &donation_unit_info_cb,
                                          NULL));

  /* D-9: the security module re-announces every donation unit on each
     restart, and h_donation_unit_pub covers the key material only.  A
     donation_units row is a historical fact -- receipts already issued under
     this key are summed from its value -- so a re-announce with a different
     year and value must leave the stored row untouched. */
  {
    struct TALER_Amount other_value;
    struct TALER_Amount stored_value;

    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (CURRENCY ":2.000020",
                                           &other_value));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            DONAUDB_insert_donation_unit (ctx,
                                          &h_donation_unit_pub,
                                          &du_pub,
                                          validity_year + 1,
                                          &other_value));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_get_donation_unit_amount (ctx,
                                              &h_donation_unit_pub,
                                              &stored_value));
    FAILIF (0 != TALER_amount_cmp (&stored_value,
                                   &du_value));
  }

  /* D-9: a key the operator retires is replaced by a *new* key, which may
     well carry the same year and value.  Both must be storable side by side,
     so that the signatures already made with the old one stay accountable. */
  {
    struct TALER_DenominationPrivateKey second_priv;
    struct TALER_DenominationPublicKey second_pub;
    struct DONAU_DonationUnitPublicKey second_du_pub;
    struct DONAU_DonationUnitHashP second_h_du_pub;

    RND_BLK (&second_h_du_pub);
    GNUNET_assert (GNUNET_OK ==
                   TALER_denom_priv_create (&second_priv,
                                            &second_pub,
                                            GNUNET_CRYPTO_BSA_RSA,
                                            RSA_KEY_SIZE));
    second_du_pub.bsign_pub_key = second_pub.bsign_pub_key;
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_insert_donation_unit (ctx,
                                          &second_h_du_pub,
                                          &second_du_pub,
                                          validity_year,
                                          &du_value));
    TALER_denom_priv_free (&second_priv);
    TALER_denom_pub_free (&second_pub);
  }

  TALER_denom_pub_free (&denom_pub);

  /* test insert signing key */
  RND_BLK (&donau_pub);
  year = GNUNET_TIME_get_current_year ();
  sk_meta.expire_legal = GNUNET_TIME_absolute_to_timestamp (
    GNUNET_TIME_year_to_time (year + 5));
  sk_meta.expire_sign = GNUNET_TIME_absolute_to_timestamp (
    GNUNET_TIME_year_to_time (year + 1));
  sk_meta.valid_from = GNUNET_TIME_absolute_to_timestamp (
    GNUNET_TIME_year_to_time (year));


  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          DONAUDB_insert_signkey (ctx,
                                  &donau_pub,
                                  &sk_meta));

  /* test iterate signing key */
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          DONAUDB_iterate_active_signkeys (ctx,
                                           &iterate_active_signkeys_cb,
                                           NULL));

  /* test insert issued receipt */
  rp = GNUNET_new (struct GNUNET_CRYPTO_BlindedMessage);
  rp->cipher = GNUNET_CRYPTO_BSA_RSA;
  rp->rc = 1;
  rsa = &rp->details.rsa_blinded_message;
  rsa->blinded_msg_size = 1 + (size_t) GNUNET_CRYPTO_random_u64 (
    (RSA_KEY_SIZE / 8) - 1);
  rsa->blinded_msg = GNUNET_malloc (rsa->blinded_msg_size);
  GNUNET_CRYPTO_random_block (rsa->blinded_msg,
                              rsa->blinded_msg_size);
  smaller_than_max_per_year = false;
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (CURRENCY ":1.000010",
                                         &amount_receipts));

  du_sigs[0].blinded_sig =
    GNUNET_CRYPTO_blind_sign (denom_priv.bsign_priv_key,
                              "rw",
                              rp);

  GNUNET_assert (NULL != du_sigs[0].blinded_sig);
  TALER_denom_priv_free (&denom_priv);

  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          DONAUDB_do_insert_receipt_issued (ctx,
                                            GNUNET_TIME_get_current_year (),
                                            num_b_sigs,
                                            du_sigs,
                                            charity_id,
                                            &h_receipt,
                                            &amount_receipts,
                                            &smaller_than_max_per_year,
                                            &charity_unknown));

  // FIXME
  // FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
  //         DONAUDB_get_receipt_issued (ctx,
  //                                         &h_receipt,
  //                                         &ir_meta));

  /* test insert submitted receipts */
  // RND_BLK (&h_donor_tax_id);
  // RND_BLK (&donation_receipts[0].h_donation_unit_pub);
  // RND_BLK (&donation_receipts[0].nonce);
  // RND_BLK (&donation_receipts[0].donation_unit_sig);
  // FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
  //         DONAUDB_insert_receipts_submitted (ctx,
  //                                            &h_donor_tax_id,
  //                                            num_dr,
  //                                            donation_receipts,
  //                                            current_year));

  DONAUDB_preflight (ctx);

  /* A transaction PostgreSQL has already aborted must never be reported as
     committed -- and a clean transaction must still return exactly
     GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, which is what every caller that
     compares against 0 relies on. */
  {
    struct GNUNET_PQ_ExecuteStatement mk[] = {
      GNUNET_PQ_make_execute ("CREATE TABLE test_commit_rollback (x INT)"),
      GNUNET_PQ_EXECUTE_STATEMENT_END
    };
    struct GNUNET_PQ_ExecuteStatement bad[] = {
      GNUNET_PQ_make_execute ("SELECT 1/0"),
      GNUNET_PQ_EXECUTE_STATEMENT_END
    };
    struct GNUNET_PQ_ExecuteStatement chk[] = {
      GNUNET_PQ_make_execute (
        "DO $$ BEGIN"
        " IF EXISTS (SELECT FROM pg_class"
        "             WHERE relname='test_commit_rollback')"
        " THEN RAISE EXCEPTION 'table survived a rollback';"
        " END IF; END $$"),
      GNUNET_PQ_EXECUTE_STATEMENT_END
    };

    FAILIF (GNUNET_OK !=
            DONAUDB_start (ctx,
                           "test-commit-aborted"));
    FAILIF (GNUNET_OK !=
            GNUNET_PQ_exec_statements (ctx->conn,
                                       mk));
    /* provoke an error; from here on the transaction is doomed */
    FAILIF (GNUNET_OK ==
            GNUNET_PQ_exec_statements (ctx->conn,
                                       bad));
    /* before the fix this returned GNUNET_DB_STATUS_SUCCESS_NO_RESULTS (0) */
    FAILIF (0 <=
            DONAUDB_commit (ctx));
    /* ...and the table is indeed gone, so 'success' would have been a lie */
    FAILIF (GNUNET_OK !=
            GNUNET_PQ_exec_statements (ctx->conn,
                                       chk));

    FAILIF (GNUNET_OK !=
            DONAUDB_start (ctx,
                           "test-commit-clean"));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            DONAUDB_commit (ctx));
  }

  /* D-5: `history' is documented as holding the *yearly* total per
     charity, so it must be able to hold more than one year per charity,
     and a repeated write for the same year must be distinguishable from
     a successful one. */
  {
    uint64_t hist_charity_id;
    struct TALER_Amount y1;
    struct TALER_Amount y2;

    FAILIF (GNUNET_OK !=
            make_charity (CURRENCY ":1000",
                          &hist_charity_id));
    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (CURRENCY ":90",
                                           &y1));
    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (CURRENCY ":75",
                                           &y2));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_insert_history_entry (ctx,
                                          hist_charity_id,
                                          &y1,
                                          2026));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_insert_history_entry (ctx,
                                          hist_charity_id,
                                          &y2,
                                          2027));
    /* Re-recording the same year must report "nothing written". */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            DONAUDB_insert_history_entry (ctx,
                                          hist_charity_id,
                                          &y2,
                                          2027));
  }

  /* D-4: the first request of a new year resets receipts_to_date; the
     closing total of the year being left must be preserved in `history'
     rather than discarded. */
  {
    uint64_t rollover_charity_id;
    struct DONAUDB_CharityMetaData rollover_meta;
    struct HistoryCheck hc;
    struct TALER_Amount zero;
    bool under_limit = false;
    uint32_t this_year = GNUNET_TIME_get_current_year ();

    FAILIF (GNUNET_OK !=
            make_charity (CURRENCY ":1000",
                          &rollover_charity_id));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            issue_receipt (rollover_charity_id,
                           this_year,
                           CURRENCY ":90",
                           &under_limit));
    FAILIF (! under_limit);
    /* Pretend those 90 were booked last year: that is the state every
       charity is in on the 1st of January, before its first request. */
    {
      struct GNUNET_PQ_QueryParam uparams[] = {
        GNUNET_PQ_query_param_uint64 (&rollover_charity_id),
        GNUNET_PQ_query_param_end
      };

      FAILIF (GNUNET_OK !=
              GNUNET_PQ_prepare_anon (ctx->conn,
                                      "UPDATE charities"
                                      "   SET current_year=current_year-1"
                                      " WHERE charity_id=$1;"));
      FAILIF (0 >=
              GNUNET_PQ_eval_prepared_non_select (ctx->conn,
                                                  "",
                                                  uparams));
    }
    /* First request of the new year. */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            issue_receipt (rollover_charity_id,
                           this_year,
                           CURRENCY ":5",
                           &under_limit));
    FAILIF (! under_limit);

    memset (&hc,
            0,
            sizeof (hc));
    hc.charity_id = (unsigned long long) rollover_charity_id;
    hc.donation_year = this_year - 1;
    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (CURRENCY ":90",
                                           &hc.expected));
    FAILIF (0 >
            DONAUDB_iterate_history_entries (ctx,
                                             &collect_history_cb,
                                             &hc));
    FAILIF (! hc.found);

    /* ... and the new year did start from zero. */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_get_charity (ctx,
                                 rollover_charity_id,
                                 &rollover_meta));
    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (CURRENCY ":5",
                                           &zero));
    FAILIF (0 != TALER_amount_cmp (&rollover_meta.receipts_to_date,
                                   &zero));
    GNUNET_free (rollover_meta.charity_name);
    GNUNET_free (rollover_meta.charity_url);
  }

  /* D-6: updating a charity to a charity_pub that another charity
     already owns must be reported as a conflict, not as "no such
     charity" -- and it must not abort the caller's transaction. */
  {
    uint64_t first_id;
    uint64_t second_id;
    struct DONAUDB_CharityMetaData first_meta;
    struct DONAUDB_CharityMetaData second_meta;
    struct TALER_Amount some_max;
    bool conflict = false;

    FAILIF (GNUNET_OK !=
            make_charity (CURRENCY ":100",
                          &first_id));
    FAILIF (GNUNET_OK !=
            make_charity (CURRENCY ":100",
                          &second_id));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_get_charity (ctx,
                                 first_id,
                                 &first_meta));
    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (CURRENCY ":100",
                                           &some_max));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_update_charity (ctx,
                                    second_id,
                                    &first_meta.charity_pub,
                                    "stolen",
                                    "https://stolen.example.com/",
                                    &some_max,
                                    &conflict));
    FAILIF (! conflict);
    /* The second charity must be untouched ... */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_get_charity (ctx,
                                 second_id,
                                 &second_meta));
    FAILIF (0 == GNUNET_memcmp (&second_meta.charity_pub,
                                &first_meta.charity_pub));
    /* ... and an unknown charity_id must still be "no results". */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            DONAUDB_update_charity (ctx,
                                    second_id + 424242,
                                    &second_meta.charity_pub,
                                    "ghost",
                                    "https://ghost.example.com/",
                                    &some_max,
                                    &conflict));
    FAILIF (conflict);
    GNUNET_free (first_meta.charity_name);
    GNUNET_free (first_meta.charity_url);
    GNUNET_free (second_meta.charity_name);
    GNUNET_free (second_meta.charity_url);
  }

  /* D-7: `receipts_submitted.nonce' is globally unique, so a donor who
     reuses one UDI nonce for a *different* donation unit in a second
     request has that receipt silently dropped.  The DB layer must tell
     the caller which index was refused, so /batch-submit can answer 409
     instead of 201. */
  {
    struct DONAU_HashDonorTaxId donor;
    struct DONAU_DonationUnitHashP du_a;
    struct DONAU_DonationUnitHashP du_b;
    struct DONAU_DonationReceipt dr;
    struct DONAU_UniqueDonorIdentifierNonce nonce;
    struct TALER_Amount total;
    struct TALER_Amount expected;
    size_t conflict_index = 42;
    size_t unknown_index = 42;
    uint64_t this_year = GNUNET_TIME_get_current_year ();

    RND_BLK (&donor);
    RND_BLK (&nonce);
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            make_donation_unit (&du_a,
                                this_year,
                                CURRENCY ":10"));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            make_donation_unit (&du_b,
                                this_year,
                                CURRENCY ":25"));

    dr.h_donation_unit_pub = du_a;
    dr.nonce = nonce;
    dr.donation_unit_sig.unblinded_sig = make_unblinded_sig ();
    FAILIF (0 >
            DONAUDB_insert_receipts_submitted (ctx,
                                               &donor,
                                               1,
                                               &dr,
                                               this_year,
                                               &conflict_index,
                                               &unknown_index));
    FAILIF (1 != conflict_index);   /* == num_dr, i.e. no conflict */
    GNUNET_CRYPTO_unblinded_sig_decref (dr.donation_unit_sig.unblinded_sig);

    /* Same donor, same nonce, different donation unit: refused. */
    dr.h_donation_unit_pub = du_b;
    dr.nonce = nonce;
    dr.donation_unit_sig.unblinded_sig = make_unblinded_sig ();
    FAILIF (0 >
            DONAUDB_insert_receipts_submitted (ctx,
                                               &donor,
                                               1,
                                               &dr,
                                               this_year,
                                               &conflict_index,
                                               &unknown_index));
    FAILIF (0 != conflict_index);
    GNUNET_CRYPTO_unblinded_sig_decref (dr.donation_unit_sig.unblinded_sig);

    /* Only the first receipt is deductible; the second one was dropped. */
    FAILIF (0 >
            DONAUDB_get_receipts_submitted_total (ctx,
                                                  this_year,
                                                  &donor,
                                                  &total));
    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (CURRENCY ":10",
                                           &expected));
    FAILIF (0 != TALER_amount_cmp (&total,
                                   &expected));
  }

  /* D-10: h_donation_unit_pub is client-supplied and is a foreign key.
     ON CONFLICT DO NOTHING does not absorb foreign key violations, so an
     unknown unit used to escape as a hard error (HTTP 500).  It must be
     reported as "unknown donation unit at index i", and nothing at all
     may be written for that batch. */
  {
    struct DONAU_HashDonorTaxId donor;
    struct DONAU_DonationUnitHashP known_du;
    struct DONAU_DonationReceipt drs[2];
    struct TALER_Amount total;
    struct TALER_Amount zero;
    size_t conflict_index = 42;
    size_t unknown_index = 42;
    uint64_t this_year = GNUNET_TIME_get_current_year ();

    RND_BLK (&donor);
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            make_donation_unit (&known_du,
                                this_year,
                                CURRENCY ":10"));
    drs[0].h_donation_unit_pub = known_du;
    RND_BLK (&drs[0].nonce);
    drs[0].donation_unit_sig.unblinded_sig = make_unblinded_sig ();
    /* Second receipt names a donation unit that was never registered. */
    RND_BLK (&drs[1].h_donation_unit_pub);
    RND_BLK (&drs[1].nonce);
    drs[1].donation_unit_sig.unblinded_sig = make_unblinded_sig ();

    FAILIF (0 >
            DONAUDB_insert_receipts_submitted (ctx,
                                               &donor,
                                               2,
                                               drs,
                                               this_year,
                                               &conflict_index,
                                               &unknown_index));
    FAILIF (1 != unknown_index);
    GNUNET_CRYPTO_unblinded_sig_decref (drs[0].donation_unit_sig.unblinded_sig);
    GNUNET_CRYPTO_unblinded_sig_decref (drs[1].donation_unit_sig.unblinded_sig);

    /* The valid receipt in the same batch must NOT have been stored. */
    FAILIF (0 >
            DONAUDB_get_receipts_submitted_total (ctx,
                                                  this_year,
                                                  &donor,
                                                  &total));
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_set_zero (CURRENCY,
                                          &zero));
    FAILIF (0 != TALER_amount_cmp (&total,
                                   &zero));
  }

  /* Everything below runs in autocommit, which is what donau-httpd does. */

  /* D-1: amount_add()'s overflow guard must trigger at 2^52, not at
     2^20.  A charity with a realistic annual limit must be able to
     book receipts well beyond 1048576 units of currency. */
  {
    uint64_t big_charity_id;
    bool under_limit = false;

    FAILIF (GNUNET_OK !=
            make_charity (CURRENCY ":4000000000",
                          &big_charity_id));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            issue_receipt (big_charity_id,
                           GNUNET_TIME_get_current_year (),
                           CURRENCY ":2000000",
                           &under_limit));
    FAILIF (! under_limit);
  }

  /* D-8: receipts_issued.receipt_id must not be settable from outside.
     If it is, an operator bulk load or a partial restore can leave the
     identity sequence behind the largest stored value, after which every
     insert fails on receipts_issued_receipt_id_key -- which donau-httpd
     used to answer with an assertion failure, i.e. abort(). */
  {
    struct GNUNET_PQ_QueryParam params[] = {
      GNUNET_PQ_query_param_end
    };

    /* Control: the very same insert without receipt_id is accepted. */
    FAILIF (GNUNET_OK !=
            GNUNET_PQ_prepare_anon (
              ctx->conn,
              "INSERT INTO receipts_issued"
              " (blinded_sig,charity_id,receipt_hash,amount)"
              " SELECT r.blinded_sig, r.charity_id,"
              "        sha512(r.receipt_hash), r.amount"
              "   FROM receipts_issued r"
              "  LIMIT 1;"));
    /* Supplying receipt_id explicitly must be refused.  With GENERATED
       ALWAYS PostgreSQL already rejects it at PREPARE time; if a server
       accepts the statement, executing it must fail. */
    if (GNUNET_OK ==
        GNUNET_PQ_prepare_anon (
          ctx->conn,
          "INSERT INTO receipts_issued"
          " (receipt_id,blinded_sig,charity_id,receipt_hash,amount)"
          " SELECT 424242, r.blinded_sig, r.charity_id,"
          "        sha512(r.receipt_hash), r.amount"
          "   FROM receipts_issued r"
          "  LIMIT 1;"))
      FAILIF (0 <=
              GNUNET_PQ_eval_prepared_non_select (ctx->conn,
                                                  "",
                                                  params));
  }

  /* D-12: between the 1st of January and a charity's first request of the
     year, `receipts_to_date' still holds last year's total.
     DONAUDB_iterate_charities() compensates for that; DONAUDB_get_charity()
     must do the same, otherwise GET /charity/$ID and GET /charities
     disagree and PATCH refuses to lower max_per_year below *last* year's
     total. */
  {
    uint64_t stale_charity_id;
    struct DONAUDB_CharityMetaData stale_meta;
    struct CharityCheck cc;
    struct TALER_Amount zero;
    bool under_limit = false;

    FAILIF (GNUNET_OK !=
            make_charity (CURRENCY ":1000",
                          &stale_charity_id));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            issue_receipt (stale_charity_id,
                           GNUNET_TIME_get_current_year (),
                           CURRENCY ":90",
                           &under_limit));
    FAILIF (! under_limit);
    {
      struct GNUNET_PQ_QueryParam uparams[] = {
        GNUNET_PQ_query_param_uint64 (&stale_charity_id),
        GNUNET_PQ_query_param_end
      };

      FAILIF (GNUNET_OK !=
              GNUNET_PQ_prepare_anon (ctx->conn,
                                      "UPDATE charities"
                                      "   SET current_year=current_year-1"
                                      " WHERE charity_id=$1;"));
      FAILIF (0 >=
              GNUNET_PQ_eval_prepared_non_select (ctx->conn,
                                                  "",
                                                  uparams));
    }
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_set_zero (CURRENCY,
                                          &zero));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_get_charity (ctx,
                                 stale_charity_id,
                                 &stale_meta));
    FAILIF (0 != TALER_amount_cmp (&stale_meta.receipts_to_date,
                                   &zero));
    GNUNET_free (stale_meta.charity_name);
    GNUNET_free (stale_meta.charity_url);

    /* ... and the two views agree. */
    memset (&cc,
            0,
            sizeof (cc));
    cc.charity_id = stale_charity_id;
    FAILIF (0 >
            DONAUDB_iterate_charities (ctx,
                                       &check_charity_cb,
                                       &cc));
    FAILIF (! cc.found);
    FAILIF (0 != TALER_amount_cmp (&cc.receipts_to_date,
                                   &zero));
  }

  /* D-13: receipts_issued.receipt_hash is the idempotence key of
     POST /batch-issue.  Deleting a charity used to cascade those rows
     away, so re-registering the charity re-enabled replay of every
     request it had ever made.  The delete must now be refused, and
     reported as such rather than as a hard error. */
  {
    uint64_t busy_charity_id;
    struct DONAUDB_CharityMetaData survivor;
    bool under_limit = false;
    bool in_use = false;

    FAILIF (GNUNET_OK !=
            make_charity (CURRENCY ":1000",
                          &busy_charity_id));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            issue_receipt (busy_charity_id,
                           GNUNET_TIME_get_current_year (),
                           CURRENCY ":7",
                           &under_limit));
    FAILIF (! under_limit);
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_delete_charity (ctx,
                                    busy_charity_id,
                                    &in_use));
    FAILIF (! in_use);
    /* The charity -- and with it the issued-receipt record -- survives. */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            DONAUDB_get_charity (ctx,
                                 busy_charity_id,
                                 &survivor));
    GNUNET_free (survivor.charity_name);
    GNUNET_free (survivor.charity_url);
    /* A charity without issued receipts is still deletable. */
    {
      uint64_t idle_charity_id;

      FAILIF (GNUNET_OK !=
              make_charity (CURRENCY ":1000",
                            &idle_charity_id));
      FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
              DONAUDB_delete_charity (ctx,
                                      idle_charity_id,
                                      &in_use));
      FAILIF (in_use);
    }
  }

  result = 0;

drop:
  if (0 != result)
    DONAUDB_rollback (ctx);
  GNUNET_break (GNUNET_OK ==
                DONAUDB_drop_tables (ctx));
cleanup:
  if (NULL != test_denom_priv.bsign_priv_key)
  {
    TALER_denom_priv_free (&test_denom_priv);
    TALER_denom_pub_free (&test_denom_pub);
  }
  DONAUDB_disconnect (ctx);
  ctx = NULL;
}


int
main (int argc,
      char *const argv[])
{
  const char *plugin_name;
  char *config_filename;
  char *testname;
  struct GNUNET_CONFIGURATION_Handle *cfg;

  (void) argc;
  result = -1;
  if (NULL == (plugin_name = strrchr (argv[0], (int) '-')))
  {
    GNUNET_break (0);
    return -1;
  }
  GNUNET_log_setup (argv[0],
                    "INFO",
                    NULL);
  plugin_name++;
  (void) GNUNET_asprintf (&testname,
                          "test-donau-db-%s",
                          plugin_name);
  (void) GNUNET_asprintf (&config_filename,
                          "%s.conf",
                          testname);
  cfg = GNUNET_CONFIGURATION_create (DONAU_project_data ());
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_parse (cfg,
                                  config_filename))
  {
    GNUNET_break (0);
    GNUNET_free (config_filename);
    GNUNET_free (testname);
    return 2;
  }
  GNUNET_SCHEDULER_run (&run,
                        cfg);
  GNUNET_CONFIGURATION_destroy (cfg);
  GNUNET_free (config_filename);
  GNUNET_free (testname);
  return result;
}


/* end of test_donaudb.c */
