/*
  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 auditordb/test_auditordb_regression.c
 * @brief regression tests for individual auditor DB operations
 * @author Christian Grothoff
 *
 * Each test function in this file pins down the behaviour of one
 * auditor DB operation that was found to be wrong.  Keep them small
 * and independent: the file is meant to grow one function per fixed
 * defect.
 */
#include <gnunet/gnunet_db_lib.h>
#include "auditordb_lib.h"
#include "auditor-database/create_tables.h"
#include "auditor-database/drop_tables.h"
#include "auditor-database/get_auditor_progress.h"
#include "auditor-database/get_balance.h"
#include "auditor-database/insert_auditor_progress.h"
#include "auditor-database/insert_balance.h"
#include "auditor-database/insert_purse_not_closed_inconsistencies.h"
#include "auditor-database/iterate_purse_not_closed_inconsistencies.h"
#include "auditor-database/preflight.h"
#include "auditor-database/start.h"

/**
 * Currency we use, must match CURRENCY in "test-auditor-db-postgres.conf".
 */
#define CURRENCY "EUR"

/**
 * Complain and fail the test if @a cond is true.
 */
#define FAILIF(cond)                            \
        do {                                    \
          if (! (cond)) break;                  \
          GNUNET_break (0);                     \
          return GNUNET_SYSERR;                 \
        } while (0)


/**
 * Global result from the testcase.
 */
static int result = -1;

/**
 * Database connection under test.
 */
static struct TALER_AUDITORDB_PostgresContext *pg;


/**
 * Parse @a str into @a amount, asserting success.
 *
 * @param str amount to parse, without currency
 * @param[out] amount where to write the result
 */
static void
amount (const char *str,
        struct TALER_Amount *amount)
{
  char buf[128];

  GNUNET_snprintf (buf,
                   sizeof (buf),
                   "%s:%s",
                   CURRENCY,
                   str);
  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount (buf,
                                         amount));
}


/**
 * A-1: neither TALER_AUDITORDB_get_balance() nor
 * TALER_AUDITORDB_get_auditor_progress() can report "key not on file"
 * (auditor_do_get_balance() emits one row per key either way), so an
 * auditor helper cannot know whether to insert or to update.  Both
 * operations must therefore store the value they are given no matter
 * which one the caller picks.
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
test_checkpoint_upsert (void)
{
  struct TALER_Amount one;
  struct TALER_Amount two;
  struct TALER_Amount got;
  uint64_t off;

  amount ("1",
          &one);
  amount ("2",
          &two);

  /* insert_balance() on a key that does not exist yet must create it. */
  FAILIF (0 >
          TALER_AUDITORDB_insert_balance (pg,
                                          "a1-update-only",
                                          &one,
                                          NULL));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_AUDITORDB_get_balance (pg,
                                       "a1-update-only",
                                       &got,
                                       NULL));
  FAILIF (0 !=
          TALER_amount_cmp (&got,
                            &one));

  /* insert_balance() on a key that already exists must overwrite it. */
  FAILIF (0 >
          TALER_AUDITORDB_insert_balance (pg,
                                          "a1-insert-twice",
                                          &one,
                                          NULL));
  FAILIF (0 >
          TALER_AUDITORDB_insert_balance (pg,
                                          "a1-insert-twice",
                                          &two,
                                          NULL));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_AUDITORDB_get_balance (pg,
                                       "a1-insert-twice",
                                       &got,
                                       NULL));
  FAILIF (0 !=
          TALER_amount_cmp (&got,
                            &two));

  /* Same two properties for the progress points. */
  FAILIF (0 >
          TALER_AUDITORDB_insert_auditor_progress (pg,
                                                   "a1-update-only",
                                                   42,
                                                   NULL));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_AUDITORDB_get_auditor_progress (pg,
                                                "a1-update-only",
                                                &off,
                                                NULL));
  FAILIF (42 != off);

  FAILIF (0 >
          TALER_AUDITORDB_insert_auditor_progress (pg,
                                                   "a1-insert-twice",
                                                   42,
                                                   NULL));
  FAILIF (0 >
          TALER_AUDITORDB_insert_auditor_progress (pg,
                                                   "a1-insert-twice",
                                                   43,
                                                   NULL));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          TALER_AUDITORDB_get_auditor_progress (pg,
                                                "a1-insert-twice",
                                                &off,
                                                NULL));
  FAILIF (43 != off);
  return GNUNET_OK;
}


/**
 * Callback for #test_purse_not_closed_dedup(), keeps the last row seen.
 *
 * @param cls a `struct TALER_AUDITORDB_PurseNotClosedInconsistencies *`
 * @param dc the row
 * @return #GNUNET_OK
 */
static enum GNUNET_GenericReturnValue
purse_not_closed_cb (
  void *cls,
  const struct TALER_AUDITORDB_PurseNotClosedInconsistencies *dc)
{
  struct TALER_AUDITORDB_PurseNotClosedInconsistencies *last = cls;

  *last = *dc;
  return GNUNET_OK;
}


/**
 * A-5: the purse auditor re-reports every expired purse on every round.
 * Without dedup on purse_pub the report table grows without bound.
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
test_purse_not_closed_dedup (void)
{
  struct TALER_AUDITORDB_PurseNotClosedInconsistencies pnc = {
    .expiration_date = GNUNET_TIME_absolute_get ()
  };
  struct TALER_AUDITORDB_PurseNotClosedInconsistencies last = { 0 };
  struct TALER_Amount three;

  GNUNET_CRYPTO_random_block (&pnc.purse_pub,
                              sizeof (pnc.purse_pub));
  amount ("1",
          &pnc.amount);
  amount ("3",
          &three);
  for (unsigned int round = 0; round < 3; round++)
  {
    if (2 == round)
      pnc.amount = three;
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            TALER_AUDITORDB_insert_purse_not_closed_inconsistencies (pg,
                                                                     &pnc));
  }
  /* Three rounds, one purse, one row -- carrying the latest amount. */
  FAILIF (1 !=
          TALER_AUDITORDB_iterate_purse_not_closed_inconsistencies (
            pg,
            1024,
            0,
            true,
            &purse_not_closed_cb,
            &last));
  FAILIF (0 !=
          TALER_amount_cmp (&last.amount,
                            &three));
  return GNUNET_OK;
}


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

  if (NULL ==
      (pg = TALER_AUDITORDB_connect (cfg)))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Failed to connect to database\n");
    result = 77;
    return;
  }
  GNUNET_assert (GNUNET_OK ==
                 TALER_AUDITORDB_preflight (pg));
  (void) TALER_AUDITORDB_drop_tables (pg,
                                      GNUNET_YES);
  if (GNUNET_OK !=
      TALER_AUDITORDB_create_tables (pg,
                                     false,
                                     0))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Failed to 'create_tables'\n");
    result = 77;
    goto unload;
  }
  if (GNUNET_SYSERR ==
      TALER_AUDITORDB_preflight (pg))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Failed preflight check\n");
    result = 77;
    goto drop;
  }
  if (GNUNET_OK !=
      TALER_AUDITORDB_start (pg,
                             "test-auditordb-regression"))
  {
    GNUNET_break (0);
    goto drop;
  }
  if (GNUNET_OK !=
      test_checkpoint_upsert ())
    goto rollback;
  if (GNUNET_OK !=
      test_purse_not_closed_dedup ())
    goto rollback;
  result = 0;
  GNUNET_break (0 <=
                TALER_AUDITORDB_commit (pg));
  goto drop;
rollback:
  TALER_AUDITORDB_rollback (pg);
drop:
  GNUNET_break (GNUNET_OK ==
                TALER_AUDITORDB_drop_tables (pg,
                                             GNUNET_YES));
unload:
  TALER_AUDITORDB_disconnect (pg);
  pg = NULL;
}


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

  (void) argc;
  result = -1;
  GNUNET_log_setup (argv[0],
                    "WARNING",
                    NULL);
  cfg = GNUNET_CONFIGURATION_create (TALER_AUDITOR_project_data ());
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_parse (cfg,
                                  "test-auditor-db-postgres.conf"))
  {
    GNUNET_break (0);
    return 2;
  }
  GNUNET_SCHEDULER_run (&run,
                        cfg);
  GNUNET_CONFIGURATION_destroy (cfg);
  return result;
}
