// Copyright 2011-2023 David Robillard <d@drobilla.net>
// SPDX-License-Identifier: ISC

#undef NDEBUG

#include "failing_allocator.h"

#include "serd/node.h"
#include "serd/nodes.h"
#include "serd/uri.h"
#include "zix/allocator.h"
#include "zix/string_view.h"

#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

static void
test_file_uri_failed_alloc(void)
{
  static const char* const string = "file://host/path/spacey%20dir/100%%.ttl";

  SerdFailingAllocator allocator = serd_failing_allocator();

  // Successfully parse a URI to count the number of allocations
  char* hostname = NULL;
  char* path     = serd_parse_file_uri(&allocator.base, string, &hostname);

  assert(!strcmp(path, "/path/spacey dir/100%.ttl"));
  assert(!strcmp(hostname, "host"));
  zix_free(&allocator.base, path);
  zix_free(&allocator.base, hostname);

  // Test that each allocation failing is handled gracefully
  const size_t n_allocs = allocator.n_allocations;
  for (size_t i = 0; i < n_allocs; ++i) {
    allocator.n_remaining = i;

    path = serd_parse_file_uri(&allocator.base, string, &hostname);
    assert(!path || !hostname);

    zix_free(&allocator.base, path);
    zix_free(&allocator.base, hostname);
  }
}

static void
test_uri_string_has_scheme(void)
{
  assert(!serd_uri_string_has_scheme("relative"));
  assert(!serd_uri_string_has_scheme("http"));
  assert(!serd_uri_string_has_scheme("5nostartdigit"));
  assert(!serd_uri_string_has_scheme("+nostartplus"));
  assert(!serd_uri_string_has_scheme("-nostartminus"));
  assert(!serd_uri_string_has_scheme(".nostartdot"));
  assert(!serd_uri_string_has_scheme(":missing"));
  assert(!serd_uri_string_has_scheme("a/slash/is/not/a/scheme/char"));

  assert(serd_uri_string_has_scheme("http://example.org/"));
  assert(serd_uri_string_has_scheme("https://example.org/"));
  assert(serd_uri_string_has_scheme("allapha:path"));
  assert(serd_uri_string_has_scheme("w1thd1g1t5:path"));
  assert(serd_uri_string_has_scheme("with.dot:path"));
  assert(serd_uri_string_has_scheme("with+plus:path"));
  assert(serd_uri_string_has_scheme("with-minus:path"));
}

static void
test_file_uri(const char* const hostname,
              const char* const path,
              const char* const expected_uri,
              const char*       expected_path)
{
  if (!expected_path) {
    expected_path = path;
  }

  SerdNodes* const nodes = serd_nodes_new(NULL);

  const SerdNode* node = serd_nodes_get(
    nodes, serd_a_file_uri(zix_string(path), zix_string(hostname)));

  const char* node_str     = serd_node_string(node);
  char*       out_hostname = NULL;
  char* const out_path     = serd_parse_file_uri(NULL, node_str, &out_hostname);

  assert(!strcmp(node_str, expected_uri));
  assert((hostname && out_hostname) || (!hostname && !out_hostname));
  assert(!hostname || !strcmp(hostname, out_hostname));
  assert(!strcmp(out_path, expected_path));

  zix_free(NULL, out_path);
  zix_free(NULL, out_hostname);
  serd_nodes_free(nodes);
}

static void
test_uri_parsing(void)
{
  test_file_uri(NULL, "C:/My 100%", "file:///C:/My%20100%%", NULL);
  test_file_uri(NULL, "/foo/bar", "file:///foo/bar", NULL);
  test_file_uri("bhost", "/foo/bar", "file://bhost/foo/bar", NULL);
  test_file_uri(NULL, "a/relative <path>", "a/relative%20%3Cpath%3E", NULL);

#ifdef _WIN32
  test_file_uri(NULL, "C:\\My 100%", "file:///C:/My%20100%%", "C:/My 100%");

  test_file_uri(
    NULL, "\\drive\\relative", "file:///drive/relative", "/drive/relative");

  test_file_uri(NULL,
                "C:\\Program Files\\Serd",
                "file:///C:/Program%20Files/Serd",
                "C:/Program Files/Serd");

  test_file_uri("ahost",
                "C:\\Pointless Space",
                "file://ahost/C:/Pointless%20Space",
                "C:/Pointless Space");
#else
  /* What happens with Windows paths on other platforms is a bit weird, but
     more or less unavoidable.  It doesn't work to interpret backslashes as
     path separators on any other platform. */

  test_file_uri("ahost",
                "C:\\Pointless Space",
                "file://ahost/C:%5CPointless%20Space",
                "/C:\\Pointless Space");

  test_file_uri(
    NULL, "\\drive\\relative", "%5Cdrive%5Crelative", "\\drive\\relative");

  test_file_uri(NULL,
                "C:\\Program Files\\Serd",
                "file:///C:%5CProgram%20Files%5CSerd",
                "/C:\\Program Files\\Serd");

  test_file_uri("ahost",
                "C:\\Pointless Space",
                "file://ahost/C:%5CPointless%20Space",
                "/C:\\Pointless Space");
#endif

  // Missing trailing '/' after authority
  assert(!serd_parse_file_uri(NULL, "file://truncated", NULL));

  // Check that NULL hostname doesn't crash
  char* out_path = serd_parse_file_uri(NULL, "file://me/path", NULL);
  assert(!strcmp(out_path, "/path"));
  zix_free(NULL, out_path);

  // Invalid first escape character
  out_path = serd_parse_file_uri(NULL, "file:///foo/%0Xbar", NULL);
  assert(!strcmp(out_path, "/foo/bar"));
  zix_free(NULL, out_path);

  // Invalid second escape character
  out_path = serd_parse_file_uri(NULL, "file:///foo/%X0bar", NULL);
  assert(!strcmp(out_path, "/foo/bar"));
  zix_free(NULL, out_path);
}

static void
test_parse_uri(void)
{
  static const ZixStringView base =
    ZIX_STATIC_STRING("http://example.org/a/b/c/");

  const SerdURIView base_uri  = serd_parse_uri(base.data);
  const SerdURIView empty_uri = serd_parse_uri("");

  SerdNodes* const nodes = serd_nodes_new(NULL);

  const SerdNode* const nil = serd_nodes_get(
    nodes, serd_a_parsed_uri(serd_resolve_uri(empty_uri, base_uri)));

  assert(serd_node_type(nil) == SERD_URI);
  assert(!strcmp(serd_node_string(nil), base.data));

  serd_nodes_free(nodes);
}

static void
check_is_within(const char* const uri_string,
                const char* const base_uri_string,
                const bool        expected)
{
  const SerdURIView uri      = serd_parse_uri(uri_string);
  const SerdURIView base_uri = serd_parse_uri(base_uri_string);

  assert(serd_uri_is_within(uri, base_uri) == expected);
}

static void
test_is_within(void)
{
  static const char* const base = "http://example.org/base/";

  check_is_within("http://example.org/base/", base, true);
  check_is_within("http://example.org/base/kid?q", base, true);
  check_is_within("http://example.org/base/kid", base, true);
  check_is_within("http://example.org/base/kid#f", base, true);
  check_is_within("http://example.org/base/kid?q#f", base, true);
  check_is_within("http://example.org/base/kid/grandkid", base, true);

  check_is_within("http://example.org/base", base, false);
  check_is_within("http://example.org/based", base, false);
  check_is_within("http://example.org/bose", base, false);
  check_is_within("http://example.org/", base, false);
  check_is_within("http://other.org/base", base, false);
  check_is_within("ftp://other.org/base", base, false);
  check_is_within("base", base, false);

  check_is_within("http://example.org/", "rel", false);
}

static inline bool
chunk_equals(const ZixStringView* a, const ZixStringView* b)
{
  return (!a->length && !b->length && !a->data && !b->data) ||
         (a->length && b->length && a->data && b->data &&
          !strncmp((const char*)a->data, (const char*)b->data, a->length));
}

static void
check_relative_uri(const char* const uri_string,
                   const char* const base_string,
                   const char* const root_string,
                   const char* const expected_string)
{
  assert(uri_string);
  assert(base_string);
  assert(expected_string);

  SerdNodes* const nodes = serd_nodes_new(NULL);
  const SerdNode*  uri_node =
    serd_nodes_get(nodes, serd_a_uri_string(uri_string));
  const SerdURIView uri = serd_node_uri_view(uri_node);
  const SerdNode*   base_node =
    serd_nodes_get(nodes, serd_a_uri_string(base_string));
  const SerdURIView base = serd_node_uri_view(base_node);

  const SerdNode* result_node = NULL;
  if (!root_string) {
    result_node =
      serd_nodes_get(nodes, serd_a_parsed_uri(serd_relative_uri(uri, base)));
  } else {
    const SerdNode* root_node =
      serd_nodes_get(nodes, serd_a_uri_string(root_string));
    const SerdURIView root = serd_node_uri_view(root_node);

    result_node =
      serd_uri_is_within(uri, root)
        ? serd_nodes_get(nodes, serd_a_parsed_uri(serd_relative_uri(uri, base)))
        : serd_nodes_get(nodes, serd_a_uri_string(uri_string));
  }

  assert(!strcmp(serd_node_string(result_node), expected_string));

  const SerdURIView result   = serd_node_uri_view(result_node);
  const SerdURIView expected = serd_parse_uri(expected_string);
  assert(chunk_equals(&result.scheme, &expected.scheme));
  assert(chunk_equals(&result.authority, &expected.authority));
  assert(chunk_equals(&result.path_prefix, &expected.path_prefix));
  assert(chunk_equals(&result.path, &expected.path));
  assert(chunk_equals(&result.query, &expected.query));
  assert(chunk_equals(&result.fragment, &expected.fragment));
  serd_nodes_free(nodes);
}

static void
test_relative_uri(void)
{
  // Unrelated base

  check_relative_uri("http://example.org/a/b",
                     "ftp://example.org/",
                     NULL,
                     "http://example.org/a/b");

  check_relative_uri("http://example.org/a/b",
                     "http://example.com/",
                     NULL,
                     "http://example.org/a/b");

  // Related base

  check_relative_uri(
    "http://example.org/a/b", "http://example.org/", NULL, "a/b");

  check_relative_uri(
    "http://example.org/a/b", "http://example.org/a/", NULL, "b");

  check_relative_uri(
    "http://example.org/a/b", "http://example.org/a/b", NULL, "");

  check_relative_uri(
    "http://example.org/a/b", "http://example.org/a/b/", NULL, "../b");

  check_relative_uri(
    "http://example.org/a/b/", "http://example.org/a/b/", NULL, "");

  check_relative_uri("http://example.org/", "http://example.org/", NULL, "");

  check_relative_uri("http://example.org/", "http://example.org/a", NULL, "");

  check_relative_uri(
    "http://example.org/", "http://example.org/a/", NULL, "../");

  check_relative_uri(
    "http://example.org/", "http://example.org/a/b", NULL, "../");

  check_relative_uri(
    "http://example.org/", "http://example.org/a/b/", NULL, "../../");

  // Unrelated root

  check_relative_uri("http://example.org/",
                     "http://example.org/a/b",
                     "relative",
                     "http://example.org/");

  check_relative_uri("http://example.org/",
                     "http://example.org/a/b",
                     "ftp://example.org/",
                     "http://example.org/");

  check_relative_uri("http://example.org/",
                     "http://example.org/a/b",
                     "http://example.com/",
                     "http://example.org/");

  // Related root

  check_relative_uri("http://example.org/a/b",
                     "http://example.org/",
                     "http://example.org/c/d",
                     "http://example.org/a/b");

  check_relative_uri("http://example.org/",
                     "http://example.org/a/b",
                     "http://example.org/a/b",
                     "http://example.org/");

  check_relative_uri("http://example.org/a/b",
                     "http://example.org/a/b",
                     "http://example.org/a/b",
                     "");

  check_relative_uri("http://example.org/a/",
                     "http://example.org/a/",
                     "http://example.org/a/",
                     "");

  check_relative_uri("http://example.org/a/b",
                     "http://example.org/a/b/c",
                     "http://example.org/a/b",
                     "../b");

  check_relative_uri("http://example.org/a",
                     "http://example.org/a/b/c",
                     "http://example.org/a/b",
                     "http://example.org/a");
}

static void
check_uri_string(const SerdURIView uri, const char* const expected)
{
  SerdNode* const node = serd_node_new(NULL, serd_a_parsed_uri(uri));
  assert(!strcmp(serd_node_string(node), expected));
  serd_node_free(NULL, node);
}

static void
test_uri_resolution(void)
{
  const ZixStringView top   = ZIX_STATIC_STRING("http://example.org/t/");
  const ZixStringView base  = ZIX_STATIC_STRING("http://example.org/t/b/");
  const ZixStringView sub   = ZIX_STATIC_STRING("http://example.org/t/b/s");
  const ZixStringView deep  = ZIX_STATIC_STRING("http://example.org/t/b/s/d");
  const ZixStringView other = ZIX_STATIC_STRING("http://example.org/o");

  const SerdURIView top_uri          = serd_parse_uri(top.data);
  const SerdURIView base_uri         = serd_parse_uri(base.data);
  const SerdURIView sub_uri          = serd_parse_uri(sub.data);
  const SerdURIView deep_uri         = serd_parse_uri(deep.data);
  const SerdURIView other_uri        = serd_parse_uri(other.data);
  const SerdURIView rel_sub_uri      = serd_relative_uri(sub_uri, base_uri);
  const SerdURIView resolved_sub_uri = serd_resolve_uri(rel_sub_uri, base_uri);

  check_uri_string(top_uri, top.data);
  check_uri_string(base_uri, base.data);
  check_uri_string(sub_uri, sub.data);
  check_uri_string(deep_uri, deep.data);
  check_uri_string(other_uri, other.data);
  check_uri_string(rel_sub_uri, "s");
  check_uri_string(resolved_sub_uri, sub.data);

  // Failure to resolve because up-reference escapes path prefix
  const SerdURIView up_uri = serd_relative_uri(resolved_sub_uri, deep_uri);
  assert(!up_uri.scheme.data);
  assert(!up_uri.scheme.length);
  assert(!up_uri.authority.data);
  assert(!up_uri.authority.length);
  assert(!up_uri.path_prefix.data);
  assert(!up_uri.path_prefix.length);
  assert(!up_uri.path.data);
  assert(!up_uri.path.length);
  assert(!up_uri.query.data);
  assert(!up_uri.query.length);
  assert(!up_uri.fragment.data);
  assert(!up_uri.fragment.length);

  // Shared path prefix is within URI path prefix
  const SerdURIView prefix_uri = serd_relative_uri(resolved_sub_uri, other_uri);
  check_uri_string(prefix_uri, "t/b/s");
}

int
main(void)
{
  test_file_uri_failed_alloc();
  test_uri_string_has_scheme();
  test_uri_parsing();
  test_parse_uri();
  test_is_within();
  test_relative_uri();
  test_uri_resolution();

  printf("Success\n");
  return 0;
}