/*
  This file is part of Anastasis
  Copyright (C) 2020, 2021 Anastasis SARL

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

  Anastasis 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 Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with
  Anastasis; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file anastasis/test_anastasis_db.c
 * @brief testcase for anastasis postgres db plugin
 * @author Marcello Stanisci
 * @author Christian Grothoff
 */
#include "platform.h"
#include "anastasis_database_lib.h"
#include "anastasis_util_lib.h"
#include <gnunet/gnunet_signatures.h>


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

#define RND_BLK(ptr)                                                    \
        GNUNET_CRYPTO_random_block (ptr, sizeof (* \
                                                 ptr))

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

/**
 * 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;
  struct TALER_Amount amount;
  struct ANASTASIS_PaymentSecretP paymentSecretP;
  struct ANASTASIS_CRYPTO_AccountPublicKeyP accountPubP;
  struct ANASTASIS_AccountSignatureP accountSig;
  struct ANASTASIS_AccountSignatureP res_account_sig;
  struct GNUNET_HashCode recoveryDataHash;
  struct GNUNET_HashCode res_recovery_data_hash;
  struct GNUNET_HashCode r;
  struct GNUNET_TIME_Relative rel_time;
  struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid;
  struct ANASTASIS_CRYPTO_EncryptedKeyShareP key_share;
  unsigned int post_counter;
  const char *mime_type;
  const char *method;
  uint32_t docVersion;
  uint32_t res_version;
  size_t recoverydatasize;
  void *res_recovery_data = NULL;
  struct ANASTASIS_CRYPTO_EncryptedKeyShareP res_key_share;
  bool paid;
  struct GNUNET_TIME_Timestamp pi_creation_date;
  bool valid_counter;
  uint32_t recversion = 1;
  unsigned char aes_gcm_tag[16];
  const char *recovery_data = "RECOVERY_DATA";
  uint64_t challenge_code = 1234;
  struct GNUNET_HashCode c_hash;
  struct ANASTASIS_UploadSignaturePS usp = {
    .purpose.purpose = htonl (GNUNET_SIGNATURE_PURPOSE_TEST),
    .purpose.size = htonl (sizeof (usp))
  };

  if (GNUNET_OK !=
      ANASTASIS_DB_init (cfg))
  {
    result = 77;
    return;
  }
  (void) ANASTASIS_DB_drop_tables ();
  if (GNUNET_OK !=
      ANASTASIS_DB_create_tables ())
  {
    result = 77;
    return;
  }
  if (GNUNET_OK !=
      ANASTASIS_DB_preflight ())
  {
    result = 77;
    return;
  }

  GNUNET_CRYPTO_hash (recovery_data,
                      strlen (recovery_data),
                      &recoveryDataHash);
  RND_BLK (&paymentSecretP);
  RND_BLK (&aes_gcm_tag);
  post_counter = 2;
  mime_type = "Picture";
  method = "Method";
  TALER_string_to_amount ("EUR:30",&amount);

  GNUNET_CRYPTO_random_block (&truth_uuid,
                              sizeof (truth_uuid));
  rel_time = GNUNET_TIME_UNIT_MONTHS;

  GNUNET_assert (GNUNET_OK ==
                 TALER_string_to_amount ("EUR:1",
                                         &amount));

  memset (&key_share, 1, sizeof (key_share));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          ANASTASIS_DB_insert_truth (
            &truth_uuid,
            &key_share,
            mime_type,
            "encrypted_truth",
            strlen ("encrypted_truth"),
            method,
            rel_time));

  /* A TOTP time step may be consumed at most once, so that a code cannot be
     replayed for as long as its window lasts. */
  {
    struct ANASTASIS_CRYPTO_TruthUUIDP unknown_uuid;

    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            ANASTASIS_DB_update_totp_counter (&truth_uuid,
                                              1000));
    /* the same step again is the replay we are guarding against */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            ANASTASIS_DB_update_totp_counter (&truth_uuid,
                                              1000));
    /* an earlier step, as a client with a slow clock would send */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            ANASTASIS_DB_update_totp_counter (&truth_uuid,
                                              999));
    /* the next step is a fresh code and must still work */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            ANASTASIS_DB_update_totp_counter (&truth_uuid,
                                              1001));
    memset (&unknown_uuid, 42, sizeof (unknown_uuid));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            ANASTASIS_DB_update_totp_counter (&unknown_uuid,
                                              1));
  }

  memset (&accountPubP, 2, sizeof (accountPubP));
  memset (&accountSig, 3, sizeof (accountSig));
  FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
          ANASTASIS_DB_get_recdoc_payment (
            &accountPubP,
            &paymentSecretP,
            &paid,
            &valid_counter,
            &pi_creation_date));

  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          ANASTASIS_DB_do_insert_recdoc_payment (
            &accountPubP,
            post_counter,
            &paymentSecretP,
            &amount));
  {
    struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
    struct GNUNET_TIME_Timestamp res_time;
    struct GNUNET_TIME_Timestamp replay_time;

    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            ANASTASIS_DB_do_update_account_lifetime (
              &accountPubP,
              &paymentSecretP,
              now,
              rel_time,
              &res_time));
    /* Lifetime is MAX(what the account had, now) + rel_time; the account
       already carries the transient lifetime insert_recdoc_payment() gave
       it, so all we can pin down is the lower bound. */
    FAILIF (GNUNET_TIME_timestamp_cmp (
              res_time,
              <,
              GNUNET_TIME_absolute_to_timestamp (
                GNUNET_TIME_absolute_add (now.abs_time,
                                          rel_time))));
    /* Replaying the same payment must not extend the lifetime again. */
    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            ANASTASIS_DB_do_update_account_lifetime (
              &accountPubP,
              &paymentSecretP,
              now,
              rel_time,
              &replay_time));
    FAILIF (GNUNET_TIME_timestamp_cmp (replay_time,
                                       !=,
                                       res_time));
  }
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          ANASTASIS_DB_get_recdoc_payment (
            &accountPubP,
            &paymentSecretP,
            &paid,
            &valid_counter,
            &pi_creation_date));
  {
    /* The same identifier under a different account must not be visible. */
    struct ANASTASIS_CRYPTO_AccountPublicKeyP otherPubP;

    memset (&otherPubP, 7, sizeof (otherPubP));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            ANASTASIS_DB_get_recdoc_payment (
              &otherPubP,
              &paymentSecretP,
              &paid,
              &valid_counter,
              &pi_creation_date));
  }
  {
    struct TALER_Amount paid_amount;
    struct TALER_Amount got_amount;

    FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
            ANASTASIS_DB_get_challenge_payment (
              &paymentSecretP,
              &truth_uuid,
              &paid,
              &got_amount));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            ANASTASIS_DB_insert_challenge_payment (
              &truth_uuid,
              &paymentSecretP,
              &amount));
    /* The wallet settles in a currency of its choosing, which need not be
       the one the order was quoted in.  What comes back out has to be
       that one, or a refund would be issued in the wrong currency. */
    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount ("CHF:23",
                                           &paid_amount));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            ANASTASIS_DB_update_to_challenge_payment_paid (
              &truth_uuid,
              &paymentSecretP,
              &paid_amount));
    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            ANASTASIS_DB_get_challenge_payment (
              &paymentSecretP,
              &truth_uuid,
              &paid,
              &got_amount));
    FAILIF (! paid);
    FAILIF (GNUNET_OK !=
            TALER_amount_cmp_currency (&paid_amount,
                                       &got_amount));
    FAILIF (0 != TALER_amount_cmp (&paid_amount,
                                   &got_amount));
  }
  FAILIF (ANASTASIS_DB_STORE_STATUS_SUCCESS !=
          ANASTASIS_DB_do_insert_recovery_document (
            &accountPubP,
            &accountSig,
            &recoveryDataHash,
            recovery_data,
            strlen (recovery_data),
            "meta-data",
            strlen ("meta-data"),
            &paymentSecretP,
            &docVersion));
  {
    uint32_t vrs;
    struct GNUNET_TIME_Timestamp exp;

    FAILIF (ANASTASIS_DB_ACCOUNT_STATUS_VALID_HASH_RETURNED !=
            ANASTASIS_DB_get_account (
              &accountPubP,
              &exp,
              &r,
              &vrs));
  }
  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          ANASTASIS_DB_get_truth_key_share (
            &truth_uuid,
            &res_key_share));
  FAILIF (0 !=
          GNUNET_memcmp (&res_key_share,
                         &key_share));

  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          ANASTASIS_DB_get_recovery_document (
            &accountPubP,
            recversion,
            &res_account_sig,
            &res_recovery_data_hash,
            &recoverydatasize,
            &res_recovery_data));
  FAILIF (0 != memcmp (res_recovery_data,
                       recovery_data,
                       strlen (recovery_data)));
  GNUNET_free (res_recovery_data);

  FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
          ANASTASIS_DB_get_latest_recovery_document (
            &accountPubP,
            &res_account_sig,
            &res_recovery_data_hash,
            &recoverydatasize,
            &res_recovery_data,
            &res_version));
  FAILIF (0 != memcmp (res_recovery_data,
                       recovery_data,
                       strlen (recovery_data)));
  GNUNET_free (res_recovery_data);

  {
    struct GNUNET_TIME_Timestamp rt;

    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            ANASTASIS_DB_insert_challenge_code (
              &truth_uuid,
              GNUNET_TIME_UNIT_HOURS,
              GNUNET_TIME_UNIT_DAYS,
              3,                              /* retry counter */
              &rt,
              &challenge_code));
    FAILIF (! GNUNET_TIME_absolute_is_zero (rt.abs_time));
  }
  {
    struct GNUNET_TIME_Timestamp rt;
    uint64_t c2;

    FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
            ANASTASIS_DB_insert_challenge_code (
              &truth_uuid,
              GNUNET_TIME_UNIT_HOURS,
              GNUNET_TIME_UNIT_DAYS,
              3,                              /* retry counter */
              &rt,
              &c2));
    FAILIF (c2 != challenge_code);
  }
  ANASTASIS_hash_answer (123,
                         &c_hash);
  {
    bool sat;
    uint64_t r_code;

    FAILIF (ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH !=
            ANASTASIS_DB_do_verify_challenge_code (
              &truth_uuid,
              &c_hash,
              &r_code,
              &sat));

    ANASTASIS_hash_answer (challenge_code,
                           &c_hash);
    FAILIF (ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED !=
            ANASTASIS_DB_do_verify_challenge_code (
              &truth_uuid,
              &c_hash,
              &r_code,
              &sat));

    /* Burn the two remaining attempts of the (only) answerable code; the
       correct answer must then be rejected as well, until a new code is
       issued. */
    ANASTASIS_hash_answer (challenge_code + 1,
                           &c_hash);
    for (unsigned int i = 0; i<2; i++)
      FAILIF (ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH !=
              ANASTASIS_DB_do_verify_challenge_code (
                &truth_uuid,
                &c_hash,
                &r_code,
                &sat));
    FAILIF (ANASTASIS_DB_CODE_STATUS_RATE_LIMITED !=
            ANASTASIS_DB_do_verify_challenge_code (
              &truth_uuid,
              &c_hash,
              &r_code,
              &sat));
    ANASTASIS_hash_answer (challenge_code,
                           &c_hash);
    FAILIF (ANASTASIS_DB_CODE_STATUS_RATE_LIMITED !=
            ANASTASIS_DB_do_verify_challenge_code (
              &truth_uuid,
              &c_hash,
              &r_code,
              &sat));
  }
  if (-1 == result)
    result = 0;

drop:
  GNUNET_break (GNUNET_OK ==
                ANASTASIS_DB_drop_tables ());
  ANASTASIS_DB_fini ();
}


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

  result = -1;
  if (NULL == (plugin_name = strrchr (argv[0], (int) '-')))
  {
    GNUNET_break (0);
    return -1;
  }
  GNUNET_log_setup (argv[0], "DEBUG", NULL);
  plugin_name++;
  GNUNET_asprintf (&testname,
                   "%s",
                   plugin_name);
  GNUNET_asprintf (&config_filename,
                   "test_anastasis_db_%s.conf",
                   testname);
  cfg = GNUNET_CONFIGURATION_create (ANASTASIS_project_data ());
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_load (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_anastasis_db.c */
