/*
  This file is part of TALER
  Copyright (C) 2026 Taler Systems SA

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

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

  You should have received a copy of the GNU Affero General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file mhd/test_legal.c
 * @brief Tests for content negotiation on legal documents
 * @author Christian Grothoff
 */
#include "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <microhttpd.h>
#include <curl/curl.h>
#include "taler/taler_util.h"
#include "taler/taler_mhd_lib.h"

/**
 * Version (Etag) of the terms we serve in this test.
 */
#define TERMS_VERSION "test-tos-v0"

/**
 * Terms of service we serve during the test.
 */
static struct TALER_MHD_Legal *legal;

/**
 * Directory with the terms of service.
 */
static char *tmpdir;

/**
 * Return value of the test.
 */
static int global_ret;


/**
 * Response headers we care about.
 */
struct Headers
{
  /**
   * Value of the "Content-Type" header, or empty.
   */
  char content_type[128];

  /**
   * Value of the "Content-Language" header, or empty.
   */
  char content_language[128];
};


/**
 * Extract @a name from the header line in @a buf of @a len bytes
 * into @a dst of @a dst_size bytes.
 */
static void
extract_header (const char *buf,
                size_t len,
                const char *name,
                char *dst,
                size_t dst_size)
{
  size_t nlen = strlen (name);
  size_t vlen;

  if ( (len < nlen + 1) ||
       (0 != strncasecmp (buf,
                          name,
                          nlen)) ||
       (':' != buf[nlen]) )
    return;
  buf += nlen + 1;
  len -= nlen + 1;
  while ( (0 != len) &&
          (isspace ((int) *buf)) )
  {
    buf++;
    len--;
  }
  while ( (0 != len) &&
          (isspace ((int) buf[len - 1])) )
    len--;
  vlen = GNUNET_MIN (len,
                     dst_size - 1);
  memcpy (dst,
          buf,
          vlen);
  dst[vlen] = '\0';
}


/**
 * Function called by CURL for each response header.
 */
static size_t
header_cb (char *buffer,
           size_t size,
           size_t nitems,
           void *userdata)
{
  struct Headers *h = userdata;
  size_t len = size * nitems;

  extract_header (buffer,
                  len,
                  "Content-Type",
                  h->content_type,
                  sizeof (h->content_type));
  extract_header (buffer,
                  len,
                  "Content-Language",
                  h->content_language,
                  sizeof (h->content_language));
  return len;
}


/**
 * Function called by CURL with the response body; we discard it.
 */
static size_t
body_cb (char *buffer,
         size_t size,
         size_t nitems,
         void *userdata)
{
  (void) buffer;
  (void) userdata;
  return size * nitems;
}


/**
 * Handle a request by returning our terms of service.
 */
static enum MHD_Result
access_handler (void *cls,
                struct MHD_Connection *connection,
                const char *url,
                const char *method,
                const char *version,
                const char *upload_data,
                size_t *upload_data_size,
                void **con_cls)
{
  static int marker;

  (void) cls;
  (void) url;
  (void) method;
  (void) version;
  (void) upload_data;
  (void) upload_data_size;
  if (NULL == *con_cls)
  {
    *con_cls = &marker;
    return MHD_YES;
  }
  return TALER_MHD_reply_legal (connection,
                                legal);
}


/**
 * Write @a content to the file @a tmpdir/@a lang/@a name.
 */
static enum GNUNET_GenericReturnValue
write_terms (const char *lang,
             const char *name,
             const char *content)
{
  char *dn;
  char *fn;
  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;

  GNUNET_asprintf (&dn,
                   "%s/%s",
                   tmpdir,
                   lang);
  GNUNET_asprintf (&fn,
                   "%s/%s",
                   dn,
                   name);
  if (GNUNET_OK ==
      GNUNET_DISK_directory_create (dn))
    ret = (GNUNET_SYSERR ==
           GNUNET_DISK_fn_write (fn,
                                 content,
                                 strlen (content),
                                 GNUNET_DISK_PERM_USER_READ
                                 | GNUNET_DISK_PERM_USER_WRITE))
          ? GNUNET_SYSERR
          : GNUNET_OK;
  GNUNET_free (fn);
  GNUNET_free (dn);
  return ret;
}


/**
 * Fetch the terms of service from the daemon at @a port using
 * @a accept and @a accept_language, and check that the response
 * carries @a want_type and @a want_language.
 *
 * @param port port the daemon listens on
 * @param accept value for the "Accept" header, NULL to send none
 * @param accept_language value for "Accept-Language", NULL to send none
 * @param want_type expected "Content-Type"
 * @param want_language expected "Content-Language"
 */
static enum GNUNET_GenericReturnValue
check_negotiation (uint16_t port,
                   const char *accept,
                   const char *accept_language,
                   const char *want_type,
                   const char *want_language)
{
  CURL *eh;
  CURLcode res;
  struct curl_slist *hdrs = NULL;
  struct Headers h = { 0 };
  char *url;
  char *hdr;
  long code;

  GNUNET_asprintf (&url,
                   "http://localhost:%u/terms",
                   (unsigned int) port);
  eh = curl_easy_init ();
  GNUNET_assert (NULL != eh);
  /* CURL always sends a default wildcard "Accept" header;
     passing the header name with an empty value drops it. */
  GNUNET_asprintf (&hdr,
                   "Accept:%s%s",
                   (NULL != accept) ? " " : "",
                   (NULL != accept) ? accept : "");
  hdrs = curl_slist_append (hdrs,
                            hdr);
  GNUNET_free (hdr);
  if (NULL != accept_language)
  {
    GNUNET_asprintf (&hdr,
                     "Accept-Language: %s",
                     accept_language);
    hdrs = curl_slist_append (hdrs,
                              hdr);
    GNUNET_free (hdr);
  }
  GNUNET_assert (CURLE_OK ==
                 curl_easy_setopt (eh, CURLOPT_URL, url));
  GNUNET_assert (CURLE_OK ==
                 curl_easy_setopt (eh, CURLOPT_HTTPHEADER, hdrs));
  GNUNET_assert (CURLE_OK ==
                 curl_easy_setopt (eh, CURLOPT_HEADERFUNCTION, &header_cb));
  GNUNET_assert (CURLE_OK ==
                 curl_easy_setopt (eh, CURLOPT_HEADERDATA, &h));
  GNUNET_assert (CURLE_OK ==
                 curl_easy_setopt (eh, CURLOPT_WRITEFUNCTION, &body_cb));
  GNUNET_assert (CURLE_OK ==
                 curl_easy_setopt (eh, CURLOPT_TIMEOUT, 30L));
  res = curl_easy_perform (eh);
  if (CURLE_OK != res)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to fetch `%s': %s\n",
                url,
                curl_easy_strerror (res));
    curl_easy_cleanup (eh);
    curl_slist_free_all (hdrs);
    GNUNET_free (url);
    return GNUNET_SYSERR;
  }
  GNUNET_assert (CURLE_OK ==
                 curl_easy_getinfo (eh, CURLINFO_RESPONSE_CODE, &code));
  curl_easy_cleanup (eh);
  curl_slist_free_all (hdrs);
  GNUNET_free (url);
  if (MHD_HTTP_OK != code)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Unexpected HTTP status %ld for Accept: %s\n",
                code,
                (NULL != accept) ? accept : "<none>");
    return GNUNET_SYSERR;
  }
  if (0 != strcmp (want_type,
                   h.content_type))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Accept: %s / Accept-Language: %s returned `%s', wanted `%s'\n",
                (NULL != accept) ? accept : "<none>",
                (NULL != accept_language) ? accept_language : "<none>",
                h.content_type,
                want_type);
    return GNUNET_SYSERR;
  }
  if (0 != strcmp (want_language,
                   h.content_language))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Accept: %s / Accept-Language: %s returned language `%s', wanted `%s'\n",
                (NULL != accept) ? accept : "<none>",
                (NULL != accept_language) ? accept_language : "<none>",
                h.content_language,
                want_language);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


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

  (void) argc;
  (void) argv;
  GNUNET_log_setup ("test-legal",
                    "WARNING",
                    NULL);
  GNUNET_assert (CURLE_OK ==
                 curl_global_init (CURL_GLOBAL_DEFAULT));
  tmpdir = GNUNET_DISK_mkdtemp ("taler-test-legal");
  GNUNET_assert (NULL != tmpdir);
  /* Order of creation is deliberately "PDF last", as that is what
     exposed the format-preference bug. */
  GNUNET_assert (GNUNET_OK ==
                 write_terms ("en",
                              TERMS_VERSION ".txt",
                              "terms in plain text"));
  GNUNET_assert (GNUNET_OK ==
                 write_terms ("en",
                              TERMS_VERSION ".html",
                              "<html><body>terms</body></html>"));
  GNUNET_assert (GNUNET_OK ==
                 write_terms ("en",
                              TERMS_VERSION ".md",
                              "# terms"));
  GNUNET_assert (GNUNET_OK ==
                 write_terms ("en",
                              TERMS_VERSION ".pdf",
                              "%PDF-1.4 terms"));
  GNUNET_assert (GNUNET_OK ==
                 write_terms ("de",
                              TERMS_VERSION ".txt",
                              "AGB im Textformat"));
  GNUNET_assert (GNUNET_OK ==
                 write_terms ("de",
                              TERMS_VERSION ".pdf",
                              "%PDF-1.4 AGB"));
  cfg = GNUNET_CONFIGURATION_create (TALER_EXCHANGE_project_data ());
  GNUNET_CONFIGURATION_set_value_string (cfg,
                                         "test",
                                         "TERMS_DIR",
                                         tmpdir);
  GNUNET_CONFIGURATION_set_value_string (cfg,
                                         "test",
                                         "TERMS_ETAG",
                                         TERMS_VERSION);
  legal = TALER_MHD_legal_load (cfg,
                                "test",
                                "TERMS_DIR",
                                "TERMS_ETAG");
  GNUNET_CONFIGURATION_destroy (cfg);
  GNUNET_assert (NULL != legal);
  d = MHD_start_daemon (MHD_USE_INTERNAL_POLLING_THREAD,
                        0 /* any free port */,
                        NULL, NULL,
                        &access_handler, NULL,
                        MHD_OPTION_END);
  GNUNET_assert (NULL != d);
  {
    const union MHD_DaemonInfo *di;

    di = MHD_get_daemon_info (d,
                              MHD_DAEMON_INFO_BIND_PORT);
    GNUNET_assert (NULL != di);
    port = di->port;
  }

  {
    struct
    {
      const char *accept;
      const char *accept_language;
      const char *want_type;
      const char *want_language;
    } tests[] = {
      /* A client without a format preference (curl, browsers, the
         wallet) must not be served the PDF. */
      { "*/*", NULL, "text/plain", "en" },
      /* No "Accept" header at all. */
      { NULL, NULL, "text/plain", "en" },
      /* A wildcard within "text" must not pick HTML over plain text. */
      { "text/*", NULL, "text/plain", "en" },
      /* An explicit preference still wins over our own ranking. */
      { "application/pdf", NULL, "application/pdf", "en" },
      { "text/html", NULL, "text/html", "en" },
      { "text/markdown", NULL, "text/markdown", "en" },
      { "text/plain;q=0.1, application/pdf;q=0.9", NULL,
        "application/pdf", "en" },
      /* Weights are the only way a client can convey a preference order,
         so they must decide both ways round -- including against our own
         ranking, which puts HTML above Markdown. */
      { "text/markdown, text/html;q=0.9", NULL, "text/markdown", "en" },
      { "text/html, text/markdown;q=0.9", NULL, "text/html", "en" },
      /* Format preference must not override the language preference. */
      { "*/*", "de", "text/plain", "de" },
      { "application/pdf", "de", "application/pdf", "de" },
      { NULL, NULL, NULL, NULL }
    };

    for (unsigned int i = 0; NULL != tests[i].want_type; i++)
      if (GNUNET_OK !=
          check_negotiation (port,
                             tests[i].accept,
                             tests[i].accept_language,
                             tests[i].want_type,
                             tests[i].want_language))
        global_ret = 1;
  }
  MHD_stop_daemon (d);
  TALER_MHD_legal_free (legal);
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_DISK_directory_remove (tmpdir));
  GNUNET_free (tmpdir);
  curl_global_cleanup ();
  return global_ret;
}


/* end of test_legal.c */
