/*
  This file is part of TALER
  Copyright (C) 2022-2023 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 testing_api_cmd_post_using_templates.c
 * @brief command to test POST /using-templates
 * @author Priscilla HUANG
 */
#include "platform.h"
#include <taler/taler_exchange_service.h>
#include <taler/taler_testing_lib.h>
#include "taler_merchant_service.h"
#include "taler_merchant_testing_lib.h"


/**
 * State of a "POST /templates" CMD.
 */
struct PostUsingTemplatesState
{

  /**
   * Handle for a "GET using-template" request.
   */
  struct TALER_MERCHANT_UsingTemplatesPostHandle *iph;

  /**
   * The interpreter state.
   */
  struct TALER_TESTING_Interpreter *is;

  /**
   * The (initial) POST /orders/$ID/claim operation handle.
   * The logic is such that after an order creation,
   * we immediately claim the order.
   */
  struct TALER_MERCHANT_OrderClaimHandle *och;

  /**
   * Base URL of the merchant serving the request.
   */
  const char *merchant_url;

  /**
   * ID of the using template to run.
   */
  const char *using_template_id;

  /**
   * Summary given by the customer.
   */
  const char *summary;

  /**
   * Amount given by the customer.
   */
  struct TALER_Amount amount;

  /**
   * Label of a command that created the template we should use.
   */
  const char *template_ref;

  /**
   * Order id.
   */
  char *order_id;

  /**
   * The order id we expect the merchant to assign (if not NULL).
   */
  const char *expected_order_id;

  /**
   * Contract terms obtained from the backend.
   */
  json_t *contract_terms;

  /**
   * Order submitted to the backend.
   */
  json_t *order_terms;

  /**
   * Contract terms hash code.
   */
  struct TALER_PrivateContractHashP h_contract_terms;

  /**
   * Merchant signature over the orders.
   */
  struct TALER_MerchantSignatureP merchant_sig;

  /**
   * Merchant public key.
   */
  struct TALER_MerchantPublicKeyP merchant_pub;

  /**
   * The nonce.
   */
  struct GNUNET_CRYPTO_EddsaPublicKey nonce;

  /**
   * The claim token
   */
  struct TALER_ClaimTokenP claim_token;

  /**
   * Should the command also CLAIM the order?
   */
  bool with_claim;

  /**
   * If not NULL, the command should duplicate the request and verify the
   * response is the same as in this command.
   */
  const char *duplicate_of;

  /**
   * Label of command creating/updating OTP device, or NULL.
   */
  const char *otp_ref;

  /**
   * Encoded key for the payment verification.
   */
  const char *otp_key;

  /**
   * Option that add amount of the order
   */
  const enum TALER_MerchantConfirmationAlgorithm *otp_alg;

  /**
   * Expected HTTP response code.
   */
  unsigned int http_status;

};

/**
 * Used to fill the "using_template" CMD state with backend-provided
 * values.  Also double-checks that the using_template was correctly
 * created.
 *
 * @param cls closure
 * @param ocr response we got
 */
static void
using_claim_cb (void *cls,
                const struct TALER_MERCHANT_OrderClaimResponse *ocr)
{
  struct PostUsingTemplatesState *tis = cls;
  const char *error_name;
  unsigned int error_line;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("merchant_pub",
                                 &tis->merchant_pub),
    GNUNET_JSON_spec_end ()
  };

  tis->och = NULL;
  if (tis->http_status != ocr->hr.http_status)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Expected status %u, got %u\n",
                tis->http_status,
                ocr->hr.http_status);
    TALER_TESTING_FAIL (tis->is);
  }
  if (MHD_HTTP_OK != ocr->hr.http_status)
  {
    TALER_TESTING_interpreter_next (tis->is);
    return;
  }
  tis->contract_terms = json_deep_copy (
    (json_t *) ocr->details.ok.contract_terms);
  tis->h_contract_terms = ocr->details.ok.h_contract_terms;
  tis->merchant_sig = ocr->details.ok.sig;
  if (GNUNET_OK !=
      GNUNET_JSON_parse (tis->contract_terms,
                         spec,
                         &error_name,
                         &error_line))
  {
    char *log;

    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Parser failed on %s:%u\n",
                error_name,
                error_line);
    log = json_dumps (tis->contract_terms,
                      JSON_INDENT (1));
    fprintf (stderr,
             "%s\n",
             log);
    free (log);
    TALER_TESTING_FAIL (tis->is);
  }
  TALER_TESTING_interpreter_next (tis->is);
}


/**
 * Callback for a POST /using-templates operation.
 *
 * @param cls closure for this function
 * @param por response being processed
 */
static void
post_using_templates_cb (void *cls,
                         const struct TALER_MERCHANT_PostOrdersReply *por)
{
  struct PostUsingTemplatesState *tis = cls;

  tis->iph = NULL;
  if (tis->http_status != por->hr.http_status)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Unexpected response code %u (%d) to command %s\n",
                por->hr.http_status,
                (int) por->hr.ec,
                TALER_TESTING_interpreter_get_current_label (tis->is));
    TALER_TESTING_interpreter_fail (tis->is);
    return;
  }
  if (0 == tis->http_status)
  {
    TALER_LOG_DEBUG ("/using_templates, expected 0 status code\n");
    TALER_TESTING_interpreter_next (tis->is);
    return;
  }
  // check for order
  switch (por->hr.http_status)
  {
  case MHD_HTTP_OK:
    if (NULL != por->details.ok.token)
      tis->claim_token = *por->details.ok.token;
    tis->order_id = GNUNET_strdup (por->details.ok.order_id);
    if ((NULL != tis->expected_order_id) &&
        (0 != strcmp (por->details.ok.order_id,
                      tis->expected_order_id)))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Order id assigned does not match\n");
      TALER_TESTING_interpreter_fail (tis->is);
      return;
    }
    if (NULL != tis->duplicate_of)
    {
      const struct TALER_TESTING_Command *order_cmd;
      const struct TALER_ClaimTokenP *prev_token;
      struct TALER_ClaimTokenP zero_token = {0};

      order_cmd = TALER_TESTING_interpreter_lookup_command (
        tis->is,
        tis->duplicate_of);
      if (GNUNET_OK !=
          TALER_TESTING_get_trait_claim_token (order_cmd,
                                               &prev_token))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                    "Could not fetch previous order claim token\n");
        TALER_TESTING_interpreter_fail (tis->is);
        return;
      }
      if (NULL == por->details.ok.token)
        prev_token = &zero_token;
      if (0 != GNUNET_memcmp (prev_token,
                              por->details.ok.token))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                    "Claim tokens for identical requests do not match\n");
        TALER_TESTING_interpreter_fail (tis->is);
        return;
      }
    }
    break;
  case MHD_HTTP_NOT_FOUND:
    TALER_TESTING_interpreter_next (tis->is);
    return;
  case MHD_HTTP_GONE:
    TALER_TESTING_interpreter_next (tis->is);
    return;
  case MHD_HTTP_CONFLICT:
    TALER_TESTING_interpreter_next (tis->is);
    return;
  default:
    {
      char *s = json_dumps (por->hr.reply,
                            JSON_COMPACT);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Unexpected status code from /orders: %u (%d) at %s; JSON: %s\n",
                  por->hr.http_status,
                  (int) por->hr.ec,
                  TALER_TESTING_interpreter_get_current_label (tis->is),
                  s);
      GNUNET_free (s);
      /**
       * Not failing, as test cases are _supposed_
       * to create non 200 OK situations.
       */
      TALER_TESTING_interpreter_next (tis->is);
    }
    return;
  }

  if (! tis->with_claim)
  {
    TALER_TESTING_interpreter_next (tis->is);
    return;
  }
  if (NULL ==
      (tis->och = TALER_MERCHANT_order_claim (
         TALER_TESTING_interpreter_get_context (tis->is),
         tis->merchant_url,
         tis->order_id,
         &tis->nonce,
         &tis->claim_token,
         &using_claim_cb,
         tis)))
    TALER_TESTING_FAIL (tis->is);
}


/**
 * Run the "POST /using-templates" CMD.
 *
 *
 * @param cls closure.
 * @param cmd command being run now.
 * @param is interpreter state.
 */
static void
post_using_templates_run (void *cls,
                          const struct TALER_TESTING_Command *cmd,
                          struct TALER_TESTING_Interpreter *is)
{
  struct PostUsingTemplatesState *tis = cls;
  const struct TALER_TESTING_Command *ref;
  const char *template_id;

  tis->is = is;
  ref = TALER_TESTING_interpreter_lookup_command (is,
                                                  tis->template_ref);
  if (GNUNET_OK !=
      TALER_TESTING_get_trait_template_id (ref,
                                           &template_id))
    TALER_TESTING_FAIL (is);
  if (NULL != tis->otp_ref)
  {
    ref = TALER_TESTING_interpreter_lookup_command (is,
                                                    tis->otp_ref);
    if (GNUNET_OK !=
        TALER_TESTING_get_trait_otp_key (ref,
                                         &tis->otp_key))
      TALER_TESTING_FAIL (is);
    if (GNUNET_OK !=
        TALER_TESTING_get_trait_otp_alg (ref,
                                         &tis->otp_alg))
      TALER_TESTING_FAIL (is);
  }
  tis->iph = TALER_MERCHANT_using_templates_post (
    TALER_TESTING_interpreter_get_context (is),
    tis->merchant_url,
    template_id,
    tis->summary,
    TALER_amount_is_valid (&tis->amount)
    ? &tis->amount
    : NULL,
    &post_using_templates_cb,
    tis);
  GNUNET_assert (NULL != tis->iph);
}


/**
 * Offers information from the POST /using-templates CMD state to other
 * commands.
 *
 * @param cls closure
 * @param[out] ret result (could be anything)
 * @param trait name of the trait
 * @param index index number of the object to extract.
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
post_using_templates_traits (void *cls,
                             const void **ret,
                             const char *trait,
                             unsigned int index)
{
  struct PostUsingTemplatesState *pts = cls;
  struct TALER_TESTING_Trait traits[] = {
    TALER_TESTING_make_trait_order_id (pts->order_id),
    TALER_TESTING_make_trait_contract_terms (pts->contract_terms),
    TALER_TESTING_make_trait_order_terms (pts->order_terms),
    TALER_TESTING_make_trait_h_contract_terms (&pts->h_contract_terms),
    TALER_TESTING_make_trait_merchant_sig (&pts->merchant_sig),
    TALER_TESTING_make_trait_merchant_pub (&pts->merchant_pub),
    TALER_TESTING_make_trait_claim_nonce (&pts->nonce),
    TALER_TESTING_make_trait_claim_token (&pts->claim_token),
    TALER_TESTING_make_trait_otp_key (pts->otp_key),
    TALER_TESTING_make_trait_otp_alg (pts->otp_alg),
    TALER_TESTING_trait_end (),
  };

  (void) pts;
  return TALER_TESTING_get_trait (traits,
                                  ret,
                                  trait,
                                  index);
}


/**
 * Free the state of a "POST using-template" CMD, and possibly
 * cancel a pending operation thereof.
 *
 * @param cls closure.
 * @param cmd command being run.
 */
static void
post_using_templates_cleanup (void *cls,
                              const struct TALER_TESTING_Command *cmd)
{
  struct PostUsingTemplatesState *tis = cls;

  if (NULL != tis->iph)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "POST /using-templates operation did not complete\n");
    TALER_MERCHANT_using_templates_post_cancel (tis->iph);
  }
  json_decref (tis->order_terms);
  json_decref (tis->contract_terms);
  GNUNET_free (tis->order_id);
  GNUNET_free (tis);
}


/**
 * Mark part of the contract terms as possible to forget.
 *
 * @param cls pointer to the result of the forget operation.
 * @param object_id name of the object to forget.
 * @param parent parent of the object at @e object_id.
 */
static void
mark_forgettable (void *cls,
                  const char *object_id,
                  json_t *parent)
{
  GNUNET_assert (GNUNET_OK ==
                 TALER_JSON_contract_mark_forgettable (parent,
                                                       object_id));
}


/**
 * Constructs the json for a POST using template request.
 *
 * @param using_template_id the name of the using_template to add, can be NULL.
 * @param refund_deadline the deadline for refunds on this using template.
 * @param pay_deadline the deadline for payment on this using template.
 * @param amount the amount this using template is for.
 * @param[out] using_template where to write the json string.
 */
static void
make_order_json (const char *using_template_id,
                 struct GNUNET_TIME_Timestamp refund_deadline,
                 struct GNUNET_TIME_Timestamp pay_deadline,
                 const char *amount,
                 json_t **using_template)
{
  struct GNUNET_TIME_Timestamp refund = refund_deadline;
  struct GNUNET_TIME_Timestamp pay = pay_deadline;
  json_t *contract_terms;
  struct TALER_Amount tamount;
  json_t *arr;

  if (NULL != amount)
    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (amount,
                                           &tamount));
  /* Include required fields and some dummy objects to test forgetting. */
  arr = json_array ();
  GNUNET_assert (NULL != arr);
  GNUNET_assert (0 ==
                 json_array_append_new (
                   arr,
                   GNUNET_JSON_PACK (
                     GNUNET_JSON_pack_string (
                       "item", "speakers"))));
  GNUNET_assert (0 ==
                 json_array_append_new (
                   arr,
                   GNUNET_JSON_PACK (
                     GNUNET_JSON_pack_string (
                       "item", "headphones"))));
  GNUNET_assert (0 ==
                 json_array_append_new (
                   arr,
                   GNUNET_JSON_PACK (
                     GNUNET_JSON_pack_string (
                       "item", "earbuds"))));
  contract_terms = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_string ("summary",
                             "merchant-lib testcase"),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string (
        "using_template_id", using_template_id)),
    NULL == amount
    ? GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("amount",
                               NULL))
    : TALER_JSON_pack_amount ("amount",
                              &tamount),
    GNUNET_JSON_pack_string ("fulfillment_url",
                             "https://example.com"),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_timestamp ("refund_deadline",
                                  refund)),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_timestamp ("pay_deadline",
                                  pay)),
    GNUNET_JSON_pack_string ("dummy_obj",
                             "EUR:1.0"),
    GNUNET_JSON_pack_array_steal ("dummy_array",
                                  arr));
  GNUNET_assert (GNUNET_OK ==
                 TALER_JSON_expand_path (contract_terms,
                                         "$.dummy_obj",
                                         &mark_forgettable,
                                         NULL));
  GNUNET_assert (GNUNET_OK ==
                 TALER_JSON_expand_path (contract_terms,
                                         "$.dummy_array[*].item",
                                         &mark_forgettable,
                                         NULL));
  *using_template = contract_terms;
}


struct TALER_TESTING_Command
TALER_TESTING_cmd_merchant_post_using_templates (
  const char *label,
  const char *template_ref,
  const char *otp_ref,
  const char *merchant_url,
  const char *using_template_id,
  const char *summary,
  const char *amount,
  struct GNUNET_TIME_Timestamp refund_deadline,
  struct GNUNET_TIME_Timestamp pay_deadline,
  unsigned int http_status)
{
  struct PostUsingTemplatesState *tis;

  tis = GNUNET_new (struct PostUsingTemplatesState);
  tis->template_ref = template_ref;
  tis->otp_ref = otp_ref;
  tis->merchant_url = merchant_url;
  tis->using_template_id = using_template_id;
  tis->http_status = http_status;
  tis->summary = summary;
  tis->with_claim = true;
  make_order_json (using_template_id,
                   refund_deadline,
                   pay_deadline,
                   amount,
                   &tis->order_terms);
  if (NULL != amount)
    GNUNET_assert (GNUNET_OK ==
                   TALER_string_to_amount (amount,
                                           &tis->amount));
  {
    struct TALER_TESTING_Command cmd = {
      .cls = tis,
      .label = label,
      .run = &post_using_templates_run,
      .cleanup = &post_using_templates_cleanup,
      .traits = &post_using_templates_traits
    };

    return cmd;
  }
}


/* end of testing_api_cmd_post_using_templates.c */
