/*
  This file is part of Anastasis
  Copyright (C) 2022 Anastasis SARL

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

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

  You should have received a copy of the GNU General Public License along with
  Anastasis; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file reducer/test_redux_codec.c
 * @brief tests for the typed reducer state codec
 * @author Christian Grothoff
 *
 * Checks three properties of ANASTASIS_REDUX_state_parse_() and
 * ANASTASIS_REDUX_state_serialize_():
 *
 * - every state the CLI test suite feeds the reducer parses;
 * - the codec is idempotent (serialize . parse . serialize == serialize .
 *   parse), so no field is lost or invented on the way through;
 * - unknown fields are rejected rather than silently dropped.
 *
 * The codec is also exercised end-to-end against live states by the `cli`
 * suite, which drives the real reducer; this test is the fast, offline part
 * that needs neither a database nor a provider.
 */
#include "platform.h"
#include "anastasis_redux.h"
#include "anastasis_api_redux.h"
#include "anastasis_api_redux_state.h"
#include <taler/taler_json_lib.h>


/**
 * Return value of the test, 0 on success.
 */
static int global_ret;


/**
 * Check that @a json parses and that the codec is idempotent on it.
 *
 * @param name name of the input, for error messages
 * @param json state to check
 */
static void
check_idempotent (const char *name,
                  const json_t *json)
{
  struct ANASTASIS_ReduxState *rs;
  enum TALER_ErrorCode ec;
  const char *detail;
  json_t *once;
  json_t *twice;

  rs = ANASTASIS_REDUX_state_parse_ (json,
                                     &ec,
                                     &detail);
  if (NULL == rs)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "%s: failed to parse at `%s'\n",
                name,
                detail);
    global_ret = 1;
    return;
  }
  once = ANASTASIS_REDUX_state_serialize_ (rs);
  GNUNET_assert (NULL != once);
  ANASTASIS_REDUX_state_free_ (rs);

  rs = ANASTASIS_REDUX_state_parse_ (once,
                                     &ec,
                                     &detail);
  if (NULL == rs)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "%s: own output failed to re-parse at `%s'\n",
                name,
                detail);
    json_decref (once);
    global_ret = 1;
    return;
  }
  twice = ANASTASIS_REDUX_state_serialize_ (rs);
  GNUNET_assert (NULL != twice);
  ANASTASIS_REDUX_state_free_ (rs);

  if (1 != json_equal (once,
                       twice))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "%s: codec is not idempotent\n",
                name);
    json_dumpf (once,
                stderr,
                JSON_INDENT (2) | JSON_SORT_KEYS);
    json_dumpf (twice,
                stderr,
                JSON_INDENT (2) | JSON_SORT_KEYS);
    global_ret = 1;
  }
  json_decref (once);
  json_decref (twice);
}


/**
 * Check that @a json is rejected by the parser.
 *
 * @param name name of the input, for error messages
 * @param json state that must not parse
 */
static void
check_rejected (const char *name,
                const json_t *json)
{
  struct ANASTASIS_ReduxState *rs;
  enum TALER_ErrorCode ec;
  const char *detail;

  rs = ANASTASIS_REDUX_state_parse_ (json,
                                     &ec,
                                     &detail);
  if (NULL != rs)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "%s: expected rejection, but the state parsed\n",
                name);
    ANASTASIS_REDUX_state_free_ (rs);
    global_ret = 1;
    return;
  }
  if (TALER_EC_ANASTASIS_REDUCER_STATE_INVALID != ec)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "%s: rejected with unexpected error code %d\n",
                name,
                (int) ec);
    global_ret = 1;
  }
}


/**
 * Run the codec over every state fixture the CLI tests use.
 *
 * @param srcdir directory holding the `resources/` fixtures
 */
static void
test_fixtures (const char *srcdir)
{
  static const char *const files[] = {
    "00-backup.json",
    "01-backup.json",
    "02-backup.json",
    "03-backup.json",
    "04-backup.json",
    "05-backup.json",
    "06-backup.json",
    "00-recovery.json",
    "01-recovery.json",
    "02-recovery.json",
    NULL
  };

  for (unsigned int i = 0; NULL != files[i]; i++)
  {
    json_error_t err;
    json_t *json;
    char *fn;

    GNUNET_asprintf (&fn,
                     "%s/resources/%s",
                     srcdir,
                     files[i]);
    json = json_load_file (fn,
                           JSON_REJECT_DUPLICATES,
                           &err);
    if (NULL == json)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to load `%s': %s\n",
                  fn,
                  err.text);
      GNUNET_free (fn);
      global_ret = 1;
      continue;
    }
    check_idempotent (files[i],
                      json);
    json_decref (json);
    GNUNET_free (fn);
  }
}


/**
 * Check that every challenge feedback variant survives the codec.
 * These are the states with the most structure (a tagged union), and
 * several of them only occur against a live provider, so they are
 * covered here rather than by the CLI tests.
 */
static void
test_challenge_feedback (void)
{
  static const char *const variants[] = {
    "{\"state\":\"code-in-file\",\"filename\":\"/tmp/tan\","
    "\"display_hint\":\"look here\"}",
    "{\"state\":\"send-to-address\",\"address_hint\":\"a@b.c\","
    "\"display_hint\":\"sent\"}",
    "{\"state\":\"send-to-address\",\"display_hint\":\"already sent\"}",
    "{\"state\":\"taler-payment\",\"taler_pay_uri\":\"taler://pay/x\","
    "\"provider\":\"http://localhost:8086/\",\"display_hint\":\"pay\","
    "\"payment_secret\":\"3F1QF6ZY2H0V4CQNVQ8YV2P8V0Y0EWWG14JBQZ3M4P2C1Y8XM53G\"}",
    "{\"state\":\"server-failure\",\"http_status\":500,\"error_code\":1}",
    "{\"state\":\"truth-unknown\",\"http_status\":404,\"error_code\":8108}",
    "{\"state\":\"iban-instructions\",\"target_iban\":\"DE12\","
    "\"target_business_name\":\"Bank\",\"wire_transfer_subject\":\"subj\","
    "\"challenge_amount\":\"TESTKUDOS:1\",\"display_hint\":\"wire it\"}",
    "{\"state\":\"solved\"}",
    "{\"state\":\"incorrect-answer\",\"error_code\":8110}",
    "{\"state\":\"rate-limit-exceeded\",\"error_code\":8111,"
    "\"request_limit\":3,\"request_frequency\":{\"d_us\":1000000},"
    "\"display_hint\":\"slow down\"}",
    NULL
  };
  static const char *const uuid
    = "3AHRZ6DMV29GQY5CKT18FPX4BJS07ENW3AHRZ6DMV29GQY5CKT18";

  for (unsigned int i = 0; NULL != variants[i]; i++)
  {
    json_error_t err;
    json_t *fb;
    json_t *state;

    fb = json_loads (variants[i],
                     JSON_REJECT_DUPLICATES,
                     &err);
    GNUNET_assert (NULL != fb);
    state = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_string ("reducer_type",
                               "recovery"),
      GNUNET_JSON_pack_string ("recovery_state",
                               "CHALLENGE_SOLVING"));
    {
      json_t *cf = json_object ();

      GNUNET_assert (NULL != cf);
      GNUNET_assert (0 ==
                     json_object_set_new (cf,
                                          uuid,
                                          fb));
      GNUNET_assert (0 ==
                     json_object_set_new (state,
                                          "challenge_feedback",
                                          cf));
    }
    check_idempotent (variants[i],
                      state);
    json_decref (state);
  }
}


/**
 * Check that fields the reducer does not know about are rejected
 * instead of being silently carried along or dropped.
 */
static void
test_strictness (void)
{
  static const char *const bad[] = {
    /* unknown field at the top level (this is the shape the TypeScript
       reducer produces; see dold.md) */
    "{\"reducer_type\":\"recovery\",\"recovery_state\":\"SECRET_SELECTING\","
    "\"verbatim_recovery_document\":{}}",
    /* unknown field inside a nested object */
    "{\"reducer_type\":\"backup\",\"backup_state\":\"CONTINENT_SELECTING\","
    "\"continents\":[{\"name\":\"Europe\",\"bogus\":1}]}",
    /* unknown provider status */
    "{\"reducer_type\":\"backup\",\"backup_state\":\"CONTINENT_SELECTING\","
    "\"authentication_providers\":{\"http://x/\":{\"status\":\"weird\"}}}",
    /* unknown challenge feedback variant */
    "{\"reducer_type\":\"recovery\",\"recovery_state\":\"CHALLENGE_SOLVING\","
    "\"challenge_feedback\":{\"3AHRZ6DMV29GQY5CKT18FPX4BJS07ENW3AHRZ6DMV29GQ"
    "Y5CKT18\":{\"state\":\"invented\"}}}",
    /* bogus state name */
    "{\"reducer_type\":\"backup\",\"backup_state\":\"NOT_A_STATE\"}",
    /* neither backup nor recovery nor error */
    "{\"reducer_type\":\"sideways\"}",
    NULL
  };

  for (unsigned int i = 0; NULL != bad[i]; i++)
  {
    json_error_t err;
    json_t *json;

    json = json_loads (bad[i],
                       JSON_REJECT_DUPLICATES,
                       &err);
    GNUNET_assert (NULL != json);
    check_rejected (bad[i],
                    json);
    json_decref (json);
  }
}


/**
 * Check that an `error` state survives the codec.
 */
static void
test_error_state (void)
{
  json_t *json;

  json = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_string ("reducer_type",
                             "error"),
    GNUNET_JSON_pack_uint64 ("code",
                             TALER_EC_ANASTASIS_REDUCER_STATE_INVALID),
    GNUNET_JSON_pack_string ("hint",
                             "a hint"),
    GNUNET_JSON_pack_string ("detail",
                             "a detail"));
  check_idempotent ("error state",
                    json);
  json_decref (json);
}


int
main (int argc,
      char *const *argv)
{
  const char *srcdir;

  (void) argc;
  GNUNET_log_setup ("test-redux-codec",
                    "WARNING",
                    NULL);
  srcdir = getenv ("ANASTASIS_CLI_SRCDIR");
  if (NULL == srcdir)
  {
    fprintf (stderr,
             "ANASTASIS_CLI_SRCDIR not set, skipping\n");
    return 77;
  }
  test_fixtures (srcdir);
  test_challenge_feedback ();
  test_strictness ();
  test_error_state ();
  return global_ret;
}


/* end of test_redux_codec.c */
