/*
  This file is part of TALER
  Copyright (C) 2020-2024 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_merchant_get_order.c
 * @brief command to test GET /private/orders/$ORDER_ID.
 * @author Jonathan Buchanan
 */
#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 for a GET /private/orders/$ORDER_ID CMD.
 */
struct MerchantGetOrderState
{
  /**
   * The merchant base URL.
   */
  const char *merchant_url;

  /**
   * Expected HTTP response code for this CMD.
   */
  unsigned int http_status;

  /**
   * The handle to the current GET /private/orders/$ORDER_ID request.
   */
  struct TALER_MERCHANT_OrderMerchantGetHandle *ogh;

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

  /**
   * Reference to a command that created an order.
   */
  const char *order_reference;

  /**
   * Expected order status.
   */
  enum TALER_MERCHANT_OrderStatusCode osc;

  /**
   * A NULL-terminated list of refunds associated with this order.
   */
  const char **refunds;

  /**
   * The length of @e refunds.
   */
  unsigned int refunds_length;

  /**
   * A NULL-terminated list of transfers associated with this order.
   */
  const char **transfers;

  /**
   * The length of @e transfers.
   */
  unsigned int transfers_length;

  /**
   * A list of forget commands that apply to this order's contract terms.
   */
  const char **forgets;

  /**
   * The length of @e forgets.
   */
  unsigned int forgets_length;

  /**
   * Set to a session ID, if we should pass one as part
   * of the request.
   */
  const char *session_id;

  /**
   * Set if we expect to be referred to another equivalent order which was
   * already paid by the wallet under this @e session_id.
   */
  const char *repurchase_order_ref;

  /**
   * Expected minimum age.
   */
  unsigned int expected_min_age;

  /**
   * True if we should pass the 'allow_refunded_for_repurchase' flag.
   */
  bool allow_refunded_for_repurchase;

  /**
   * Whether the order was refunded or not.
   */
  bool refunded;

  /**
   * Whether the order was wired or not.
   */
  bool wired;
};


/**
 * Forget part of the contract terms.
 *
 * @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
apply_forget (void *cls,
              const char *object_id,
              json_t *parent)
{
  int *res = cls;

  if (GNUNET_SYSERR ==
      TALER_JSON_contract_part_forget (parent,
                                       object_id))
    *res = GNUNET_SYSERR;
}


/**
 * Callback to process a GET /orders/$ID request
 *
 * @param cls closure
 * @param osr order status response details
 */
static void
merchant_get_order_cb (
  void *cls,
  const struct TALER_MERCHANT_OrderStatusResponse *osr)
{
  struct MerchantGetOrderState *gos = cls;

  gos->ogh = NULL;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "GET /private/orders/$ID completed with status %u\n",
              osr->hr.http_status);
  if (gos->http_status != osr->hr.http_status)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Unexpected response code %u (%d) to command %s\n",
                osr->hr.http_status,
                (int) osr->hr.ec,
                TALER_TESTING_interpreter_get_current_label (gos->is));
    TALER_TESTING_interpreter_fail (gos->is);
    return;
  }
  switch (osr->hr.http_status)
  {
  case MHD_HTTP_OK:
    if (gos->osc != osr->details.ok.status)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Order paid does not match: %d vs %d\n",
                  gos->osc,
                  osr->details.ok.status);
      TALER_TESTING_interpreter_fail (gos->is);
      return;
    }
    switch (osr->details.ok.status)
    {
    case TALER_MERCHANT_OSC_PAID:
      {
        const struct TALER_TESTING_Command *order_cmd;
        struct TALER_Amount refunded_total;

        if ( (0 != gos->expected_min_age) &&
             (gos->expected_min_age !=
              json_integer_value (
                json_object_get (
                  osr->details.ok.details.paid.contract_terms,
                  "minimum_age"))) )
        {
          GNUNET_break (0);
          TALER_TESTING_interpreter_fail (gos->is);
          return;
        }
        order_cmd = TALER_TESTING_interpreter_lookup_command (
          gos->is,
          gos->order_reference);

        {
          const json_t *expected_contract_terms;
          json_t *ct;

          if (GNUNET_OK !=
              TALER_TESTING_get_trait_contract_terms (order_cmd,
                                                      &expected_contract_terms))
          {
            GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                        "Could not fetch order contract terms\n");
            TALER_TESTING_interpreter_fail (gos->is);
            return;
          }

          ct = json_deep_copy (expected_contract_terms);

          /* Apply all forgets, then compare */
          for (unsigned int i = 0; i < gos->forgets_length; ++i)
          {
            const struct TALER_TESTING_Command *forget_cmd;
            const uint32_t *paths_length;

            forget_cmd = TALER_TESTING_interpreter_lookup_command (
              gos->is,
              gos->forgets[i]);

            if (GNUNET_OK !=
                TALER_TESTING_get_trait_paths_length (forget_cmd,
                                                      &paths_length))
            {
              GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                          "Couldn't fetch forget paths length\n");
              TALER_TESTING_interpreter_fail (gos->is);
              return;
            }

            for (unsigned int j = 0; j < *paths_length; ++j)
            {
              const char *path;
              int res = GNUNET_OK;

              if (GNUNET_OK !=
                  TALER_TESTING_get_trait_paths (forget_cmd,
                                                 j,
                                                 &path))
              {
                GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                            "Couldn't fetch forget path\n");
                TALER_TESTING_interpreter_fail (gos->is);
                return;
              }

              GNUNET_assert (GNUNET_OK ==
                             TALER_JSON_expand_path (ct,
                                                     path,
                                                     &apply_forget,
                                                     &res));
              GNUNET_assert (GNUNET_OK == res);
            }
          }

          if (1 != json_equal (ct,
                               osr->details.ok.details.paid.contract_terms))
          {
            GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                        "Order contract terms do not match\n");
            TALER_TESTING_interpreter_fail (gos->is);
            return;
          }

          json_decref (ct);
        }
        if (gos->wired != osr->details.ok.details.paid.wired)
        {
          GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                      "Order wired does not match\n");
          TALER_TESTING_interpreter_fail (gos->is);
          return;
        }
        if (gos->transfers_length != osr->details.ok.details.paid.wts_len)
        {
          GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                      "Number of transfers found does not match\n");
          TALER_TESTING_interpreter_fail (gos->is);
          return;
        }
        for (unsigned int i = 0; i < gos->transfers_length; ++i)
        {
          const struct TALER_TESTING_Command *transfer_cmd;

          transfer_cmd = TALER_TESTING_interpreter_lookup_command (
            gos->is,
            gos->transfers[i]);
          {
            const struct TALER_WireTransferIdentifierRawP *wtid;

            if (GNUNET_OK !=
                TALER_TESTING_get_trait_wtid (transfer_cmd,
                                              &wtid))
            {
              GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                          "Could not fetch wire transfer id\n");
              TALER_TESTING_interpreter_fail (gos->is);
              return;
            }
            if (0 != GNUNET_memcmp (wtid,
                                    &osr->details.ok.details.paid.wts[i].
                                    wtid))
            {
              GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                          "Wire transfer id does not match\n");
              TALER_TESTING_interpreter_fail (gos->is);
              return;
            }
          }
          {
            const char *exchange_url;

            if (GNUNET_OK !=
                TALER_TESTING_get_trait_exchange_url (transfer_cmd,
                                                      &exchange_url))
            {
              GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                          "Could not fetch wire transfer exchange url\n");
              TALER_TESTING_interpreter_fail (gos->is);
              return;
            }
            if (0 != strcmp (
                  exchange_url,
                  osr->details.ok.details.paid.wts[i].exchange_url))
            {
              GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                          "Wire transfer exchange url does not match\n");
              TALER_TESTING_interpreter_fail (gos->is);
              return;
            }
          }
        }
        if (gos->refunded != osr->details.ok.details.paid.refunded)
        {
          GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                      "Order refunded does not match\n");
          TALER_TESTING_interpreter_fail (gos->is);
          return;
        }
        if (gos->refunds_length !=
            osr->details.ok.details.paid.refunds_len)
        {
          GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                      "Number of refunds found does not match\n");
          TALER_TESTING_interpreter_fail (gos->is);
          return;
        }
        if (0 < gos->refunds_length)
          GNUNET_assert (
            GNUNET_OK ==
            TALER_amount_set_zero (
              osr->details.ok.details.paid.refund_amount.currency,
              &refunded_total));
        for (unsigned int i = 0; i < gos->refunds_length; ++i)
        {
          const struct TALER_TESTING_Command *refund_cmd;

          refund_cmd = TALER_TESTING_interpreter_lookup_command (
            gos->is,
            gos->refunds[i]);
          {
            const struct TALER_Amount *expected_amount;
            struct TALER_Amount *amount_found =
              &osr->details.ok.details.paid.refunds[i].refund_amount;

            if (GNUNET_OK !=
                TALER_TESTING_get_trait_amount (refund_cmd,
                                                &expected_amount))
            {
              GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                          "Could not fetch refund amount\n");
              TALER_TESTING_interpreter_fail (gos->is);
              return;
            }
            GNUNET_assert (0 <= TALER_amount_add (&refunded_total,
                                                  &refunded_total,
                                                  amount_found));
            if ((GNUNET_OK !=
                 TALER_amount_cmp_currency (expected_amount,
                                            &refunded_total)) ||
                (0 != TALER_amount_cmp (expected_amount,
                                        &refunded_total)))
            {
              GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                          "Refund amounts do not match\n");
              TALER_TESTING_interpreter_fail (gos->is);
              return;
            }
          }
          {
            const char *expected_reason;

            if (GNUNET_OK !=
                TALER_TESTING_get_trait_reason (refund_cmd,
                                                &expected_reason))
            {
              GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                          "Could not fetch reason\n");
              TALER_TESTING_interpreter_fail (gos->is);
              return;
            }
            if (0 !=
                strcmp (
                  expected_reason,
                  osr->details.ok.details.paid.refunds[i].reason))
            {
              GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                          "Refund reason does not match\n");
              TALER_TESTING_interpreter_fail (gos->is);
              return;
            }
          }
        }

        if (gos->wired != osr->details.ok.details.paid.wired)
        {
          GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                      "Order wired does not match\n");
          TALER_TESTING_interpreter_fail (gos->is);
          return;
        }
      }
      break;
    case TALER_MERCHANT_OSC_CLAIMED:
      /* FIXME: Check contract terms... */
      if ( (0 != gos->expected_min_age) &&
           (gos->expected_min_age !=
            json_integer_value (
              json_object_get (
                osr->details.ok.details.claimed.contract_terms,
                "minimum_age"))) )
      {
        GNUNET_break (0);
        TALER_TESTING_interpreter_fail (gos->is);
        return;
      }
      break;
    case TALER_MERCHANT_OSC_UNPAID:
      {
        struct TALER_MERCHANT_PayUriData pud;
        const struct TALER_TESTING_Command *order_cmd;
        const char *order_id;
        const struct TALER_ClaimTokenP *claim_token;

        if (NULL != gos->repurchase_order_ref)
        {
          const struct TALER_TESTING_Command *rep_cmd;
          const char *rep_id;
          const char *ri;

          rep_cmd = TALER_TESTING_interpreter_lookup_command (
            gos->is,
            gos->repurchase_order_ref);
          if (GNUNET_OK !=
              TALER_TESTING_get_trait_order_id (rep_cmd,
                                                &rep_id))
          {
            TALER_TESTING_FAIL (gos->is);
          }
          ri = osr->details.ok.details.unpaid.already_paid_order_id;
          if ( (NULL == ri) ||
               (0 !=
                strcmp (ri,
                        rep_id)) )
          {
            TALER_TESTING_FAIL (gos->is);
          }
        }

        if (GNUNET_OK !=
            TALER_MERCHANT_parse_pay_uri (
              osr->details.ok.details.unpaid.taler_pay_uri,
              &pud))
        {
          GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                      "Taler pay uri `%s' is malformed\n",
                      osr->details.ok.details.unpaid.taler_pay_uri);
          TALER_TESTING_interpreter_fail (gos->is);
          return;
        }

        order_cmd = TALER_TESTING_interpreter_lookup_command (
          gos->is,
          gos->order_reference);

        if (GNUNET_OK !=
            TALER_TESTING_get_trait_order_id (order_cmd,
                                              &order_id))
        {
          TALER_MERCHANT_parse_pay_uri_free (&pud);
          TALER_TESTING_FAIL (gos->is);
        }

        if (GNUNET_OK !=
            TALER_TESTING_get_trait_claim_token (order_cmd,
                                                 &claim_token))
        {
          TALER_MERCHANT_parse_pay_uri_free (&pud);
          TALER_TESTING_FAIL (gos->is);
        }
        {
          char *host;

          host = TALER_MERCHANT_TESTING_extract_host (gos->merchant_url);
          if ((0 != strcmp (host,
                            pud.merchant_host)) ||
              (NULL != pud.merchant_prefix_path) ||
              (0 != strcmp (order_id,
                            pud.order_id)) ||
              (NULL != pud.ssid))
          {
            GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                        "Order pay uri `%s' does not match, wanted %s/%s\n",
                        osr->details.ok.details.unpaid.taler_pay_uri,
                        host,
                        order_id);
            TALER_TESTING_interpreter_fail (gos->is);
            TALER_MERCHANT_parse_pay_uri_free (&pud);
            GNUNET_free (host);
            return;
          }
          GNUNET_free (host);
        }
        /* The claim token is not given in the pay uri if the order
           has been claimed already. */
        if ( (NULL != pud.claim_token) &&
             ( (NULL == claim_token) ||
               (0 != GNUNET_memcmp (claim_token,
                                    pud.claim_token)) ) )
        {
          GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                      "Order pay uri claim token does not match (%d/%d/%d/%d)\n",
                      NULL == pud.claim_token,
                      NULL == claim_token,
                      (NULL != pud.claim_token) &&
                      GNUNET_is_zero (pud.claim_token),
                      (NULL != claim_token) &&
                      GNUNET_is_zero (claim_token));
          TALER_TESTING_interpreter_fail (gos->is);
          TALER_MERCHANT_parse_pay_uri_free (&pud);
          return;
        }
        TALER_MERCHANT_parse_pay_uri_free (&pud);
        break;
      }
    }
    break;
  default:
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Unhandled HTTP status.\n");
  }
  TALER_TESTING_interpreter_next (gos->is);
}


/**
 * Run the "GET order" CMD.
 *
 * @param cls closure.
 * @param cmd command being run now.
 * @param is interpreter state.
 */
static void
merchant_get_order_run (void *cls,
                        const struct TALER_TESTING_Command *cmd,
                        struct TALER_TESTING_Interpreter *is)
{
  struct MerchantGetOrderState *gos = cls;
  const struct TALER_TESTING_Command *order_cmd;
  const char *order_id;
  const struct TALER_PrivateContractHashP *h_contract;

  order_cmd = TALER_TESTING_interpreter_lookup_command (
    is,
    gos->order_reference);

  if (GNUNET_OK !=
      TALER_TESTING_get_trait_order_id (order_cmd,
                                        &order_id))
    TALER_TESTING_FAIL (is);

  if (GNUNET_OK !=
      TALER_TESTING_get_trait_h_contract_terms (order_cmd,
                                                &h_contract))
    TALER_TESTING_FAIL (is);

  gos->is = is;
  gos->ogh = TALER_MERCHANT_merchant_order_get (
    TALER_TESTING_interpreter_get_context (is),
    gos->merchant_url,
    order_id,
    gos->session_id,
    GNUNET_TIME_UNIT_ZERO,
    &merchant_get_order_cb,
    gos);
}


/**
 * Free the state of a "GET order" CMD, and possibly
 * cancel a pending operation thereof.
 *
 * @param cls closure.
 * @param cmd command being run.
 */
static void
merchant_get_order_cleanup (void *cls,
                            const struct TALER_TESTING_Command *cmd)
{
  struct MerchantGetOrderState *gos = cls;

  if (NULL != gos->ogh)
  {
    TALER_LOG_WARNING ("Get order operation did not complete\n");
    TALER_MERCHANT_merchant_order_get_cancel (gos->ogh);
  }
  GNUNET_array_grow (gos->transfers,
                     gos->transfers_length,
                     0);
  GNUNET_array_grow (gos->refunds,
                     gos->refunds_length,
                     0);
  GNUNET_array_grow (gos->forgets,
                     gos->forgets_length,
                     0);
  GNUNET_free (gos);
}


struct TALER_TESTING_Command
TALER_TESTING_cmd_merchant_get_order (
  const char *label,
  const char *merchant_url,
  const char *order_reference,
  enum TALER_MERCHANT_OrderStatusCode osc,
  bool refunded,
  unsigned int http_status,
  ...)
{
  struct MerchantGetOrderState *gos;

  gos = GNUNET_new (struct MerchantGetOrderState);
  gos->merchant_url = merchant_url;
  gos->order_reference = order_reference;
  gos->osc = osc;
  gos->refunded = refunded;
  gos->http_status = http_status;
  if (refunded)
  {
    const char *clabel;
    va_list ap;

    va_start (ap, http_status);
    while (NULL != (clabel = va_arg (ap, const char *)))
    {
      GNUNET_array_append (gos->refunds,
                           gos->refunds_length,
                           clabel);
    }
    va_end (ap);
  }
  {
    struct TALER_TESTING_Command cmd = {
      .cls = gos,
      .label = label,
      .run = &merchant_get_order_run,
      .cleanup = &merchant_get_order_cleanup
    };

    return cmd;
  }
}


struct TALER_TESTING_Command
TALER_TESTING_cmd_merchant_get_order2 (
  const char *label,
  const char *merchant_url,
  const char *order_reference,
  enum TALER_MERCHANT_OrderStatusCode osc,
  bool wired,
  const char **transfers,
  bool refunded,
  const char **refunds,
  const char **forgets,
  unsigned int http_status)
{
  struct MerchantGetOrderState *gos;

  gos = GNUNET_new (struct MerchantGetOrderState);
  gos->merchant_url = merchant_url;
  gos->order_reference = order_reference;
  gos->osc = osc;
  gos->wired = wired;
  gos->refunded = refunded;
  gos->http_status = http_status;
  if (wired)
  {
    for (const char **clabel = transfers; *clabel != NULL; ++clabel)
    {
      GNUNET_array_append (gos->transfers,
                           gos->transfers_length,
                           *clabel);
    }
  }
  if (refunded)
  {
    for (const char **clabel = refunds; *clabel != NULL; ++clabel)
    {
      GNUNET_array_append (gos->refunds,
                           gos->refunds_length,
                           *clabel);
    }
  }
  if (NULL != forgets)
  {
    for (const char **clabel = forgets; *clabel != NULL; ++clabel)
    {
      GNUNET_array_append (gos->forgets,
                           gos->forgets_length,
                           *clabel);
    }
  }
  {
    struct TALER_TESTING_Command cmd = {
      .cls = gos,
      .label = label,
      .run = &merchant_get_order_run,
      .cleanup = &merchant_get_order_cleanup
    };

    return cmd;
  }
}


struct TALER_TESTING_Command
TALER_TESTING_cmd_merchant_get_order3 (
  const char *label,
  const char *merchant_url,
  const char *order_reference,
  enum TALER_MERCHANT_OrderStatusCode osc,
  const char *session_id,
  const char *repurchase_order_ref,
  unsigned int expected_http_status)
{
  struct MerchantGetOrderState *gos;

  gos = GNUNET_new (struct MerchantGetOrderState);
  gos->merchant_url = merchant_url;
  gos->order_reference = order_reference;
  gos->osc = osc;
  gos->session_id = session_id;
  gos->repurchase_order_ref = repurchase_order_ref;
  gos->http_status = expected_http_status;
  {
    struct TALER_TESTING_Command cmd = {
      .cls = gos,
      .label = label,
      .run = &merchant_get_order_run,
      .cleanup = &merchant_get_order_cleanup
    };

    return cmd;
  }
}


struct TALER_TESTING_Command
TALER_TESTING_cmd_merchant_get_order4 (
  const char *label,
  const char *merchant_url,
  const char *order_reference,
  enum TALER_MERCHANT_OrderStatusCode osc,
  uint32_t expected_min_age,
  unsigned int expected_http_status)
{
  struct MerchantGetOrderState *gos;

  gos = GNUNET_new (struct MerchantGetOrderState);
  gos->merchant_url = merchant_url;
  gos->order_reference = order_reference;
  gos->osc = osc;
  gos->expected_min_age = expected_min_age;
  gos->http_status = expected_http_status;
  {
    struct TALER_TESTING_Command cmd = {
      .cls = gos,
      .label = label,
      .run = &merchant_get_order_run,
      .cleanup = &merchant_get_order_cleanup
    };

    return cmd;
  }
}


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

  /**
   * Reference to a command that can provide a poll order start command.
   */
  const char *start_reference;

  /**
   * Task to wait for the deadline.
   */
  struct GNUNET_SCHEDULER_Task *task;

  /**
   * Expected HTTP response status code.
   */
  unsigned int expected_http_status;
};


struct MerchantPollOrderStartState
{
  /**
   * The merchant base URL.
   */
  const char *merchant_url;

  /**
   * The handle to the current GET /private/orders/$ORDER_ID request.
   */
  struct TALER_MERCHANT_OrderMerchantGetHandle *ogh;

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

  /**
   * Reference to a command that created an order.
   */
  const char *order_id;

  /**
   * How long to wait for server to return a response.
   */
  struct GNUNET_TIME_Relative timeout;

  /**
   * Conclude state waiting for completion (if any).
   */
  struct MerchantPollOrderConcludeState *cs;

  /**
   * The HTTP status code returned by the backend.
   */
  unsigned int http_status;

  /**
   * When the request should be completed by.
   */
  struct GNUNET_TIME_Absolute deadline;
};


/**
 * Task called when either the timeout for the GET /private/order/$ID command
 * expired or we got a response.  Checks if the result is what we expected.
 *
 * @param cls a `struct MerchantPollOrderConcludeState`
 */
static void
conclude_task (void *cls)
{
  struct MerchantPollOrderConcludeState *ppc = cls;
  const struct TALER_TESTING_Command *poll_cmd;
  struct MerchantPollOrderStartState *cps;
  struct GNUNET_TIME_Absolute now;

  ppc->task = NULL;
  poll_cmd =
    TALER_TESTING_interpreter_lookup_command (ppc->is,
                                              ppc->start_reference);
  if (NULL == poll_cmd)
    TALER_TESTING_FAIL (ppc->is);
  cps = poll_cmd->cls;
  if (NULL != cps->ogh)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Expected poll GET /private/orders/$ORDER_ID to have completed, but it did not!\n");
    TALER_TESTING_FAIL (ppc->is);
  }
  if (cps->http_status != ppc->expected_http_status)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Expected HTTP status %u, got %u\n",
                ppc->expected_http_status,
                cps->http_status);
    TALER_TESTING_FAIL (ppc->is);
  }
  now = GNUNET_TIME_absolute_get ();
  if ((GNUNET_TIME_absolute_add (cps->deadline,
                                 GNUNET_TIME_UNIT_SECONDS).abs_value_us <
       now.abs_value_us) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Expected answer to be delayed until %llu, but got response at %llu\n",
                (unsigned long long) cps->deadline.abs_value_us,
                (unsigned long long) now.abs_value_us);
    TALER_TESTING_FAIL (ppc->is);
  }
  TALER_TESTING_interpreter_next (ppc->is);
}


/**
 * Callback to process a GET /private/orders/$ID request
 *
 * @param cls closure
 * @param osr order status response details
 */
static void
merchant_poll_order_cb (
  void *cls,
  const struct TALER_MERCHANT_OrderStatusResponse *osr)
{
  struct MerchantPollOrderStartState *pos = cls;
  const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr;

  pos->ogh = NULL;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "GET /private/orders/$ID finished with status %u.\n",
              hr->http_status);
  pos->http_status = hr->http_status;
  switch (hr->http_status)
  {
  case MHD_HTTP_OK:
    // FIXME: keep data from 'osr' here for checking?
    break;
  default:
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Unhandled HTTP status.\n");
  }
  if (NULL != pos->cs)
  {
    GNUNET_SCHEDULER_cancel (pos->cs->task);
    pos->cs->task = GNUNET_SCHEDULER_add_now (&conclude_task,
                                              pos->cs);
  }
}


/**
 * Run the "GET order" CMD.
 *
 * @param cls closure.
 * @param cmd command being run now.
 * @param is interpreter state.
 */
static void
merchant_poll_order_start_run (void *cls,
                               const struct TALER_TESTING_Command *cmd,
                               struct TALER_TESTING_Interpreter *is)
{
  struct MerchantPollOrderStartState *pos = cls;

  /* add 1s grace time to timeout */
  pos->deadline
    = GNUNET_TIME_absolute_add (GNUNET_TIME_relative_to_absolute (pos->timeout),
                                GNUNET_TIME_UNIT_SECONDS);
  pos->is = is;
  pos->ogh = TALER_MERCHANT_merchant_order_get (
    TALER_TESTING_interpreter_get_context (is),
    pos->merchant_url,
    pos->order_id,
    NULL,
    pos->timeout,
    &merchant_poll_order_cb,
    pos);
  GNUNET_assert (NULL != pos->ogh);
  /* We CONTINUE to run the interpreter while the long-polled command
     completes asynchronously! */
  TALER_TESTING_interpreter_next (pos->is);
}


/**
 * Free the state of a "GET order" CMD, and possibly
 * cancel a pending operation thereof.
 *
 * @param cls closure.
 * @param cmd command being run.
 */
static void
merchant_poll_order_start_cleanup (void *cls,
                                   const struct TALER_TESTING_Command *cmd)
{
  struct MerchantPollOrderStartState *pos = cls;

  if (NULL != pos->ogh)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Command `%s' was not terminated\n",
                TALER_TESTING_interpreter_get_current_label (
                  pos->is));
    TALER_MERCHANT_merchant_order_get_cancel (pos->ogh);
  }
  GNUNET_free (pos);
}


struct TALER_TESTING_Command
TALER_TESTING_cmd_poll_order_start (
  const char *label,
  const char *merchant_url,
  const char *order_id,
  struct GNUNET_TIME_Relative timeout)
{
  struct MerchantPollOrderStartState *pos;

  pos = GNUNET_new (struct MerchantPollOrderStartState);
  pos->order_id = order_id;
  pos->merchant_url = merchant_url;
  pos->timeout = timeout;
  {
    struct TALER_TESTING_Command cmd = {
      .cls = pos,
      .label = label,
      .run = &merchant_poll_order_start_run,
      .cleanup = &merchant_poll_order_start_cleanup
    };

    return cmd;
  }
}


/**
 * Run the "GET order conclude" CMD.
 *
 * @param cls closure.
 * @param cmd command being run now.
 * @param is interpreter state.
 */
static void
merchant_poll_order_conclude_run (void *cls,
                                  const struct TALER_TESTING_Command *cmd,
                                  struct TALER_TESTING_Interpreter *is)
{
  struct MerchantPollOrderConcludeState *poc = cls;
  const struct TALER_TESTING_Command *poll_cmd;
  struct MerchantPollOrderStartState *pos;

  poc->is = is;
  poll_cmd =
    TALER_TESTING_interpreter_lookup_command (is,
                                              poc->start_reference);
  if (NULL == poll_cmd)
    TALER_TESTING_FAIL (poc->is);
  GNUNET_assert (poll_cmd->run == &merchant_poll_order_start_run);
  pos = poll_cmd->cls;
  pos->cs = poc;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Waiting on GET /private/orders/$ID of %s (%s)\n",
              poc->start_reference,
              (NULL == pos->ogh)
              ? "finished"
              : "active");
  if (NULL == pos->ogh)
    poc->task = GNUNET_SCHEDULER_add_now (&conclude_task,
                                          poc);
  else
    poc->task = GNUNET_SCHEDULER_add_at (pos->deadline,
                                         &conclude_task,
                                         poc);
}


/**
 * Free the state of a "GET order" CMD, and possibly
 * cancel a pending operation thereof.
 *
 * @param cls closure.
 * @param cmd command being run.
 */
static void
merchant_poll_order_conclude_cleanup (void *cls,
                                      const struct TALER_TESTING_Command *cmd)
{
  struct MerchantPollOrderConcludeState *poc = cls;

  if (NULL != poc->task)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Command `%s' was not terminated\n",
                TALER_TESTING_interpreter_get_current_label (
                  poc->is));
    GNUNET_SCHEDULER_cancel (poc->task);
    poc->task = NULL;
  }
  GNUNET_free (poc);
}


struct TALER_TESTING_Command
TALER_TESTING_cmd_poll_order_conclude (const char *label,
                                       unsigned int http_status,
                                       const char *poll_start_reference)
{
  struct MerchantPollOrderConcludeState *cps;

  cps = GNUNET_new (struct MerchantPollOrderConcludeState);
  cps->start_reference = poll_start_reference;
  cps->expected_http_status = http_status;
  {
    struct TALER_TESTING_Command cmd = {
      .cls = cps,
      .label = label,
      .run = &merchant_poll_order_conclude_run,
      .cleanup = &merchant_poll_order_conclude_cleanup
    };

    return cmd;
  }
}


/* end of testing_api_cmd_merchant_get_order.c */
