/*
  This file is part of Challenger
  (C) 2023 Taler Systems SA

  Challenger 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.

  Challenger 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
  Challenger; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file challengerdb/test_challenger_db.c
 * @brief testcase for challenger postgres db plugin
 * @author Christian Grothoff
 */
#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_pq_lib.h>
#include <taler/taler_util.h>
#include "challenger_database_lib.h"
#include "challenger-database/drop_tables.h"
#include "challenger-database/create_tables.h"
#include "challenger-database/preflight.h"
#include "challenger-database/gc.h"
#include "challenger-database/insert_client.h"
#include "challenger-database/do_insert_validation.h"
#include "challenger-database/do_challenge_address.h"
#include "challenger-database/do_solve_challenge.h"
#include "challenger-database/do_insert_token.h"
#include "challenger-database/get_token.h"
#include "challenger-database/get_validation_pkce.h"
#include "challenger_util.h"
#include "pg_helper.h"
#include "challenger-database/delete_client.h"
#include "challenger-database/do_insert_validation.h"


#define FAILIF(cond)                            \
        do {                                          \
          if (! (cond)) { break;}                       \
          GNUNET_break (0);                           \
          goto drop;                                     \
        } while (0)

/**
 * Secret of the client the tests register and act as.
 */
#define CLIENT_SECRET "secret-token:test-secret"

/**
 * Redirect URI of the client the tests register.
 */
#define CLIENT_URI "http://client.example.com/"

/**
 * Global return value for the test.  Initially -1, set to 0 upon
 * completion.   Other values indicate some kind of error.
 */
static int result;

/**
 * Handle to the database we are testing.
 */
static struct CHALLENGERDB_PostgresContext *pg;

/**
 * Serial ID of the client registered by #setup_client().
 */
static uint64_t client_id;


/**
 * Register the OAuth client all tests act as.
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
setup_client (void)
{
  bool uri_taken = false;

  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
      CHALLENGERDB_insert_client (pg,
                                  CLIENT_URI,
                                  CLIENT_SECRET,
                                  &client_id,
                                  &uri_taken))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Create a fresh validation for #client_id and have a PIN transmitted
 * for it, leaving the validation in the state a user reaches by posting
 * an address to ``/challenge`` and then walking away.
 *
 * @param[out] nonce set to the nonce identifying the new validation
 * @param[out] pin set to the PIN that was "transmitted"
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
challenge_validation (struct CHALLENGER_ValidationNonceP *nonce,
                      uint32_t *pin)
{
  struct GNUNET_TIME_Absolute expiration
    = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
  json_t *address;
  char *state = NULL;
  char *redirect_uri = NULL;
  struct GNUNET_TIME_Absolute last_tx_time;
  uint32_t auth_attempts_left;
  uint32_t pin_transmissions_left;
  bool pin_transmit;
  bool address_refused;
  bool solved;
  enum GNUNET_DB_QueryStatus qs;

  GNUNET_CRYPTO_random_block (nonce,
                              sizeof (*nonce));
  qs = CHALLENGERDB_do_insert_validation (pg,
                                          client_id,
                                          CLIENT_SECRET,
                                          nonce,
                                          expiration,
                                          NULL);
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  address = json_pack ("{s:s}",
                       "filename",
                       "test-challenger-db.txt");
  GNUNET_assert (NULL != address);
  *pin = 424242;
  qs = CHALLENGERDB_do_challenge_address (pg,
                                          nonce,
                                          address,
                                          GNUNET_TIME_UNIT_ZERO,
                                          pin,
                                          &state,
                                          &last_tx_time,
                                          &auth_attempts_left,
                                          &pin_transmissions_left,
                                          &pin_transmit,
                                          &redirect_uri,
                                          &address_refused,
                                          &solved);
  json_decref (address);
  /* The PIN is only pending until its transmission is confirmed; this
     stands in for a successful AUTH_COMMAND run. */
  qs = CHALLENGERDB_do_challenge_address_confirm_pin (pg,
                                                      nonce,
                                                      &auth_attempts_left);
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  GNUNET_free (state);
  GNUNET_free (redirect_uri);
  if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) ||
       (! pin_transmit) ||
       address_refused ||
       solved)
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Enter @a pin for the validation under @a nonce and check it was accepted.
 *
 * @param nonce validation to solve
 * @param pin PIN to enter
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
solve_validation (const struct CHALLENGER_ValidationNonceP *nonce,
                  uint32_t pin)
{
  char *state = NULL;
  char *redirect_uri = NULL;
  uint32_t addr_left;
  uint32_t auth_attempts_left;
  uint32_t pin_transmissions_left;
  bool solved;
  bool exhausted;
  bool no_challenge;
  enum GNUNET_DB_QueryStatus qs;

  qs = CHALLENGERDB_do_solve_challenge (pg,
                                        nonce,
                                        pin,
                                        &solved,
                                        &exhausted,
                                        &no_challenge,
                                        &state,
                                        &addr_left,
                                        &auth_attempts_left,
                                        &pin_transmissions_left,
                                        &redirect_uri);
  GNUNET_free (state);
  GNUNET_free (redirect_uri);
  if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) ||
       (! solved) ||
       exhausted ||
       no_challenge)
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Mint an access token for the validation under @a nonce.
 *
 * @param client client redeeming the authorization code
 * @param nonce validation to redeem
 * @param[out] token set to the (random) access token we tried to store
 * @return transaction status of CHALLENGERDB_do_insert_token()
 */
static enum GNUNET_DB_QueryStatus
insert_token (uint64_t client,
              const struct CHALLENGER_ValidationNonceP *nonce,
              struct CHALLENGER_AccessTokenP *token)
{
  GNUNET_CRYPTO_random_block (token,
                              sizeof (*token));
  return CHALLENGERDB_do_insert_token (pg,
                                       nonce,
                                       client,
                                       token,
                                       GNUNET_TIME_UNIT_HOURS,
                                       GNUNET_TIME_UNIT_DAYS);
}


/**
 * Test that a validation for which the user never entered the correct
 * PIN cannot be redeemed for an access token, and is not even visible to
 * the ``/token`` lookup.  Without the ``auth_attempts_left < 0`` guard
 * anyone able to compute the authorization code could mint an
 * attestation for an address nobody ever proved control over.
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
test_unsolved_not_redeemable (void)
{
  struct CHALLENGER_ValidationNonceP nonce;
  struct CHALLENGER_AccessTokenP token;
  uint32_t pin;

  if (GNUNET_OK !=
      challenge_validation (&nonce,
                            &pin))
    return GNUNET_SYSERR;
  {
    char *client_secret;
    json_t *address;
    char *client_scope;
    char *client_state;
    char *client_redirect_uri;
    char *code_challenge;
    uint32_t code_challenge_method;

    if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
        CHALLENGERDB_get_validation_pkce (pg,
                                          &nonce,
                                          client_id,
                                          &client_secret,
                                          &address,
                                          &client_scope,
                                          &client_state,
                                          &client_redirect_uri,
                                          &code_challenge,
                                          &code_challenge_method))
    {
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
      insert_token (client_id,
                    &nonce,
                    &token))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Test that a solved validation is redeemable, and redeemable exactly
 * once (the authorization code must not be replayable).
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
test_solved_redeemable (void)
{
  struct CHALLENGER_ValidationNonceP nonce;
  struct CHALLENGER_AccessTokenP token;
  uint32_t pin;

  if ( (GNUNET_OK !=
        challenge_validation (&nonce,
                              &pin)) ||
       (GNUNET_OK !=
        solve_validation (&nonce,
                          pin)) )
    return GNUNET_SYSERR;
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
      insert_token (client_id,
                    &nonce,
                    &token))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  /* replay: the validation was consumed, so this must not mint a token */
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
      insert_token (client_id,
                    &nonce,
                    &token))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Test that the address expiration stored with a freshly minted token is
 * in the future, and in particular that it does *not* depend on the
 * validation's ``last_tx_time``.  The regression this guards against
 * computed the expiration as ``address_expiration + last_tx_time``, which
 * lands in 1970 whenever that column was never written.  We force the
 * column back to 0 to pin the property down without relying on any
 * particular code path leaving it there.
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
test_address_expiry_is_in_the_future (void)
{
  struct GNUNET_PQ_ExecuteStatement es[] = {
    GNUNET_PQ_make_execute ("UPDATE validations SET last_tx_time=0;"),
    GNUNET_PQ_EXECUTE_STATEMENT_END
  };
  struct CHALLENGER_ValidationNonceP nonce;
  struct CHALLENGER_AccessTokenP token;
  struct GNUNET_TIME_Timestamp address_expiration;
  struct GNUNET_TIME_Absolute now;
  json_t *address = NULL;
  uint64_t rowid;
  uint32_t pin;
  enum GNUNET_GenericReturnValue ret;

  if ( (GNUNET_OK !=
        challenge_validation (&nonce,
                              &pin)) ||
       (GNUNET_OK !=
        solve_validation (&nonce,
                          pin)) )
    return GNUNET_SYSERR;
  if (GNUNET_OK !=
      GNUNET_PQ_exec_statements (pg->conn,
                                 es))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  now = GNUNET_TIME_absolute_get ();
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
      insert_token (client_id,
                    &nonce,
                    &token))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
      CHALLENGERDB_get_token (pg,
                              &token,
                              &rowid,
                              &address,
                              &address_expiration))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  ret = GNUNET_OK;
  if (address_expiration.abs_time.abs_value_us <= now.abs_value_us)
  {
    GNUNET_break (0);
    ret = GNUNET_SYSERR;
  }
  json_decref (address);
  return ret;
}


/**
 * Test that an expired validation is not consumable, even though the
 * caller (``/token``) checked the expiration in an earlier, separate
 * transaction.  Everything get_validation_pkce() filters on has to be
 * repeated by do_insert_token(), or the gap between the two statements is
 * an unguarded TOCTOU window.
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
test_expired_not_redeemable (void)
{
  struct GNUNET_PQ_ExecuteStatement es[] = {
    GNUNET_PQ_make_execute ("UPDATE validations SET expiration_time=1;"),
    GNUNET_PQ_EXECUTE_STATEMENT_END
  };
  struct CHALLENGER_ValidationNonceP nonce;
  struct CHALLENGER_AccessTokenP token;
  uint32_t pin;

  if ( (GNUNET_OK !=
        challenge_validation (&nonce,
                              &pin)) ||
       (GNUNET_OK !=
        solve_validation (&nonce,
                          pin)) )
    return GNUNET_SYSERR;
  if (GNUNET_OK !=
      GNUNET_PQ_exec_statements (pg->conn,
                                 es))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
      insert_token (client_id,
                    &nonce,
                    &token))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Test that a validation cannot be consumed by a client it does not
 * belong to.
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
test_foreign_client_not_redeemable (void)
{
  struct CHALLENGER_ValidationNonceP nonce;
  struct CHALLENGER_AccessTokenP token;
  uint32_t pin;

  if ( (GNUNET_OK !=
        challenge_validation (&nonce,
                              &pin)) ||
       (GNUNET_OK !=
        solve_validation (&nonce,
                          pin)) )
    return GNUNET_SYSERR;
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
      insert_token (client_id + 1,
                    &nonce,
                    &token))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  /* ... and the rightful client still can */
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
      insert_token (client_id,
                    &nonce,
                    &token))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Run @a sql directly on the database, bypassing the CHALLENGERDB API.
 * Used to create states (and to make assertions about the schema) that
 * the API deliberately cannot produce.
 *
 * @param sql SQL statement to execute
 * @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);
}


/**
 * Test that a client cannot be inserted silently: a URI that is already
 * registered must be reported as such, and any *other* unique violation
 * (notably one on the primary key, which happens once the identity sequence
 * of 'clients' lags behind max(client_serial_id)) must not be misreported as
 * a duplicate URI.
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
test_insert_client (void)
{
  uint64_t nclient_id;
  bool uri_taken;

  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
      CHALLENGERDB_insert_client (pg,
                                  "https://example.com/a",
                                  "secret-token:a",
                                  &nclient_id,
                                  &uri_taken))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (uri_taken)
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  /* Same URI again: this one really is a duplicate. */
  if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
        CHALLENGERDB_insert_client (pg,
                                    "https://example.com/a",
                                    "secret-token:b",
                                    &nclient_id,
                                    &uri_taken)) ||
       (! uri_taken) )
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  /* Rewind the identity sequence, as a restore that did not restore the
     sequences leaves it: the next INSERT then collides on the primary key,
     not on the URI. */
  if (GNUNET_OK !=
      exec_sql ("DO $$ BEGIN"
                " PERFORM setval("
                "   pg_get_serial_sequence('challenger.clients',"
                "                          'client_serial_id'),"
                "   (SELECT MIN(client_serial_id) FROM challenger.clients),"
                "   false);"
                "END $$;"))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
      CHALLENGERDB_insert_client (pg,
                                  "https://example.com/b",
                                  "secret-token:b",
                                  &nclient_id,
                                  &uri_taken))
  {
    /* The PK collision must not go unnoticed. */
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (uri_taken)
  {
    /* This is the regression: the failure has nothing to do with the URI,
       and the operator must not be told that the URI already exists. */
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  /* Repair the sequence, then the very same insert must work. */
  if (GNUNET_OK !=
      exec_sql ("DO $$ BEGIN"
                " PERFORM setval("
                "   pg_get_serial_sequence('challenger.clients',"
                "                          'client_serial_id'),"
                "   (SELECT MAX(client_serial_id) FROM challenger.clients),"
                "   true);"
                "END $$;"))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
        CHALLENGERDB_insert_client (pg,
                                    "https://example.com/b",
                                    "secret-token:b",
                                    &nclient_id,
                                    &uri_taken)) ||
       (uri_taken) )
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Test that deleting a client reports how many validations it took with it:
 * validations_client_serial_id_fkey cascades, so an operator would otherwise
 * abort in-flight KYC processes without being told.
 *
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
test_delete_client (void)
{
  const char *uri = "https://example.com/del";
  const char *secret = "secret-token:del";
  struct GNUNET_TIME_Absolute expiration
    = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS);
  uint64_t nclient_id;
  uint64_t validations_deleted;
  bool uri_taken;

  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
      CHALLENGERDB_insert_client (pg,
                                  uri,
                                  secret,
                                  &nclient_id,
                                  &uri_taken))
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  for (unsigned int i = 0; i < 2; i++)
  {
    struct CHALLENGER_ValidationNonceP nonce;

    GNUNET_CRYPTO_random_block (&nonce,
                                sizeof (nonce));
    if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
        CHALLENGERDB_do_insert_validation (pg,
                                           nclient_id,
                                           secret,
                                           &nonce,
                                           expiration,
                                           NULL))
    {
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
  }
  if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
        CHALLENGERDB_delete_client (pg,
                                    uri,
                                    &validations_deleted)) ||
       (2 != validations_deleted) )
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  /* Deleting it again finds nothing and discards nothing. */
  if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
        CHALLENGERDB_delete_client (pg,
                                    uri,
                                    &validations_deleted)) ||
       (0 != validations_deleted) )
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  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;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Connecting\n");
  if (NULL == (pg = CHALLENGERDB_connect_admin (cfg)))
  {
    result = 77;
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Connected\n");
  if (GNUNET_OK !=
      CHALLENGERDB_drop_tables (pg))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Dropping tables failed\n");
  }
  if (GNUNET_OK !=
      CHALLENGERDB_create_tables (pg))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Creating tables failed\n");
    goto drop;
  }
  GNUNET_assert (GNUNET_OK ==
                 CHALLENGERDB_preflight (pg));
  {
    struct GNUNET_TIME_Absolute ts = GNUNET_TIME_absolute_get ();

    FAILIF (0 >
            CHALLENGERDB_gc (pg,
                             ts));
  }
  FAILIF (GNUNET_OK !=
          setup_client ());
  FAILIF (GNUNET_OK !=
          test_unsolved_not_redeemable ());
  FAILIF (GNUNET_OK !=
          test_solved_redeemable ());
  FAILIF (GNUNET_OK !=
          test_address_expiry_is_in_the_future ());
  FAILIF (GNUNET_OK !=
          test_expired_not_redeemable ());
  FAILIF (GNUNET_OK !=
          test_foreign_client_not_redeemable ());
  FAILIF (GNUNET_OK !=
          test_insert_client ());
  FAILIF (GNUNET_OK !=
          test_delete_client ());
  result = 0;
drop:
  GNUNET_break (GNUNET_OK ==
                CHALLENGERDB_drop_tables (pg));
  CHALLENGERDB_disconnect (pg);
  pg = NULL;
}


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

  (void) argc;
  result = EXIT_FAILURE;
  GNUNET_log_setup (argv[0],
                    "DEBUG",
                    NULL);
  cfg = GNUNET_CONFIGURATION_create (CHALLENGER_project_data ());
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_parse (cfg,
                                  "test_challenger_db_postgres.conf"))
  {
    GNUNET_break (0);
    return EXIT_NOTCONFIGURED;
  }
  GNUNET_SCHEDULER_run (&run, cfg);
  GNUNET_CONFIGURATION_destroy (cfg);
  return result;
}


/* end of test_challenger_db.c */
