/*
  Copyright 2011-2016 David Robillard <d@drobilla.net>

  Permission to use, copy, modify, and/or distribute this software for any
  purpose with or without fee is hereby granted, provided that the above
  copyright notice and this permission notice appear in all copies.

  THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include "serd/serd.h"
#include "sord/sord.h"

#include <inttypes.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef __GNUC__
#  define SORD_LOG_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1)))
#else
#  define SORD_LOG_FUNC(fmt, arg1)
#endif

static const int      DIGITS        = 3;
static const unsigned n_objects_per = 2;

static int n_expected_errors = 0;

typedef struct {
  SordQuad query;
  int      expected_num_results;
} QueryTest;

#define USTR(s) ((const uint8_t*)(s))

static SordNode*
uri(SordWorld* world, int num)
{
  if (num == 0) {
    return 0;
  }

  char  str[]   = "eg:000";
  char* uri_num = str + 3; // First `0'
  snprintf(uri_num, DIGITS + 1, "%0*d", DIGITS, num);
  return sord_new_uri(world, (const uint8_t*)str);
}

SORD_LOG_FUNC(1, 2)
static int
test_fail(const char* fmt, ...)
{
  va_list args;
  va_start(args, fmt);
  fprintf(stderr, "error: ");
  vfprintf(stderr, fmt, args);
  va_end(args);
  return 1;
}

static int
generate(SordWorld* world, SordModel* sord, size_t n_quads, SordNode* graph)
{
  fprintf(stderr,
          "Generating %zu (S P *) quads with %u objects each\n",
          n_quads,
          n_objects_per);

  for (size_t i = 0; i < n_quads; ++i) {
    int num = (i * n_objects_per) + 1;

    SordNode* ids[2 + n_objects_per];
    for (unsigned j = 0; j < 2 + n_objects_per; ++j) {
      ids[j] = uri(world, num++);
    }

    for (unsigned j = 0; j < n_objects_per; ++j) {
      SordQuad tup = {ids[0], ids[1], ids[2 + j], graph};
      if (!sord_add(sord, tup)) {
        return test_fail("Fail: Failed to add quad\n");
      }
    }

    for (unsigned j = 0; j < 2 + n_objects_per; ++j) {
      sord_node_free(world, ids[j]);
    }
  }

  // Add some literals

  // (98 4 "hello") and (98 4 "hello"^^<5>)
  SordQuad tup = {0, 0, 0, 0};
  tup[0]       = uri(world, 98);
  tup[1]       = uri(world, 4);
  tup[2]       = sord_new_literal(world, 0, USTR("hello"), NULL);
  tup[3]       = graph;
  sord_add(sord, tup);
  sord_node_free(world, (SordNode*)tup[2]);
  tup[2] = sord_new_literal(world, uri(world, 5), USTR("hello"), NULL);
  if (!sord_add(sord, tup)) {
    return test_fail("Failed to add typed literal\n");
  }

  // (96 4 "hello"^^<4>) and (96 4 "hello"^^<5>)
  tup[0] = uri(world, 96);
  tup[1] = uri(world, 4);
  tup[2] = sord_new_literal(world, uri(world, 4), USTR("hello"), NULL);
  tup[3] = graph;
  sord_add(sord, tup);
  sord_node_free(world, (SordNode*)tup[2]);
  tup[2] = sord_new_literal(world, uri(world, 5), USTR("hello"), NULL);
  if (!sord_add(sord, tup)) {
    return test_fail("Failed to add typed literal\n");
  }

  // (94 5 "hello") and (94 5 "hello"@en-gb)
  tup[0] = uri(world, 94);
  tup[1] = uri(world, 5);
  tup[2] = sord_new_literal(world, 0, USTR("hello"), NULL);
  tup[3] = graph;
  sord_add(sord, tup);
  sord_node_free(world, (SordNode*)tup[2]);
  tup[2] = sord_new_literal(world, NULL, USTR("hello"), "en-gb");
  if (!sord_add(sord, tup)) {
    return test_fail("Failed to add literal with language\n");
  }

  // (92 6 "hello"@en-us) and (92 5 "hello"@en-gb)
  tup[0] = uri(world, 92);
  tup[1] = uri(world, 6);
  tup[2] = sord_new_literal(world, 0, USTR("hello"), "en-us");
  tup[3] = graph;
  sord_add(sord, tup);
  sord_node_free(world, (SordNode*)tup[2]);
  tup[2] = sord_new_literal(world, NULL, USTR("hello"), "en-gb");
  if (!sord_add(sord, tup)) {
    return test_fail("Failed to add literal with language\n");
  }

  sord_node_free(world, (SordNode*)tup[0]);
  sord_node_free(world, (SordNode*)tup[2]);
  tup[0] = uri(world, 14);
  tup[2] = sord_new_literal(world, 0, USTR("bonjour"), "fr");
  sord_add(sord, tup);
  sord_node_free(world, (SordNode*)tup[2]);
  tup[2] = sord_new_literal(world, 0, USTR("salut"), "fr");
  sord_add(sord, tup);

  // Attempt to add some duplicates
  if (sord_add(sord, tup)) {
    return test_fail("Fail: Successfully added duplicate quad\n");
  }
  if (sord_add(sord, tup)) {
    return test_fail("Fail: Successfully added duplicate quad\n");
  }

  // Add a blank node subject
  sord_node_free(world, (SordNode*)tup[0]);
  tup[0] = sord_new_blank(world, USTR("ablank"));
  sord_add(sord, tup);

  sord_node_free(world, (SordNode*)tup[1]);
  sord_node_free(world, (SordNode*)tup[2]);
  tup[1] = uri(world, 6);
  tup[2] = uri(world, 7);
  sord_add(sord, tup);
  sord_node_free(world, (SordNode*)tup[0]);
  sord_node_free(world, (SordNode*)tup[1]);
  sord_node_free(world, (SordNode*)tup[2]);

  return EXIT_SUCCESS;
}

#define TUP_FMT "(%6s %6s %6s)"
#define TUP_FMT_ARGS(t)                                  \
  ((t)[0] ? sord_node_get_string((t)[0]) : USTR("*")),   \
    ((t)[1] ? sord_node_get_string((t)[1]) : USTR("*")), \
    ((t)[2] ? sord_node_get_string((t)[2]) : USTR("*"))

static int
test_read(SordWorld* world, SordModel* sord, SordNode* g, const size_t n_quads)
{
  int ret = EXIT_SUCCESS;

  SordQuad id;

  SordIter* iter = sord_begin(sord);
  if (sord_iter_get_model(iter) != sord) {
    return test_fail("Fail: Iterator has incorrect sord pointer\n");
  }

  for (; !sord_iter_end(iter); sord_iter_next(iter)) {
    sord_iter_get(iter, id);
  }

  // Attempt to increment past end
  if (!sord_iter_next(iter)) {
    return test_fail("Fail: Successfully incremented past end\n");
  }

  sord_iter_free(iter);

  const uint8_t* s           = USTR("hello");
  SordNode*      plain_hello = sord_new_literal(world, 0, s, NULL);
  SordNode*      type4_hello = sord_new_literal(world, uri(world, 4), s, NULL);
  SordNode*      type5_hello = sord_new_literal(world, uri(world, 5), s, NULL);
  SordNode*      gb_hello    = sord_new_literal(world, NULL, s, "en-gb");
  SordNode*      us_hello    = sord_new_literal(world, NULL, s, "en-us");

#define NUM_PATTERNS 18

  QueryTest patterns[NUM_PATTERNS] = {
    {{0, 0, 0}, (int)(n_quads * n_objects_per) + 12},
    {{uri(world, 1), 0, 0}, 2},
    {{uri(world, 9), uri(world, 9), uri(world, 9)}, 0},
    {{uri(world, 1), uri(world, 2), uri(world, 4)}, 1},
    {{uri(world, 3), uri(world, 4), uri(world, 0)}, 2},
    {{uri(world, 0), uri(world, 2), uri(world, 4)}, 1},
    {{uri(world, 0), uri(world, 0), uri(world, 4)}, 1},
    {{uri(world, 1), uri(world, 0), uri(world, 0)}, 2},
    {{uri(world, 1), uri(world, 0), uri(world, 4)}, 1},
    {{uri(world, 0), uri(world, 2), uri(world, 0)}, 2},
    {{uri(world, 98), uri(world, 4), plain_hello}, 1},
    {{uri(world, 98), uri(world, 4), type5_hello}, 1},
    {{uri(world, 96), uri(world, 4), type4_hello}, 1},
    {{uri(world, 96), uri(world, 4), type5_hello}, 1},
    {{uri(world, 94), uri(world, 5), plain_hello}, 1},
    {{uri(world, 94), uri(world, 5), gb_hello}, 1},
    {{uri(world, 92), uri(world, 6), gb_hello}, 1},
    {{uri(world, 92), uri(world, 6), us_hello}, 1}};

  SordQuad match = {uri(world, 1), uri(world, 2), uri(world, 4), g};
  if (!sord_contains(sord, match)) {
    return test_fail("Fail: No match for " TUP_FMT "\n", TUP_FMT_ARGS(match));
  }

  SordQuad nomatch = {uri(world, 1), uri(world, 2), uri(world, 9), g};
  if (sord_contains(sord, nomatch)) {
    return test_fail("Fail: False match for " TUP_FMT "\n",
                     TUP_FMT_ARGS(nomatch));
  }

  if (sord_get(sord, NULL, NULL, uri(world, 3), g)) {
    return test_fail("Fail: Get *,*,3 succeeded\n");
  } else if (!sord_node_equals(
               sord_get(sord, uri(world, 1), uri(world, 2), NULL, g),
               uri(world, 3))) {
    return test_fail("Fail: Get 1,2,* != 3\n");
  } else if (!sord_node_equals(
               sord_get(sord, uri(world, 1), NULL, uri(world, 3), g),
               uri(world, 2))) {
    return test_fail("Fail: Get 1,*,3 != 2\n");
  } else if (!sord_node_equals(
               sord_get(sord, NULL, uri(world, 2), uri(world, 3), g),
               uri(world, 1))) {
    return test_fail("Fail: Get *,2,3 != 1\n");
  }

  for (unsigned i = 0; i < NUM_PATTERNS; ++i) {
    QueryTest test = patterns[i];
    SordQuad  pat  = {test.query[0], test.query[1], test.query[2], g};
    fprintf(stderr, "Query " TUP_FMT "... ", TUP_FMT_ARGS(pat));

    iter            = sord_find(sord, pat);
    int num_results = 0;
    for (; !sord_iter_end(iter); sord_iter_next(iter)) {
      sord_iter_get(iter, id);
      ++num_results;
      if (!sord_quad_match(pat, id)) {
        sord_iter_free(iter);
        return test_fail("Fail: Query result " TUP_FMT
                         " does not match pattern\n",
                         TUP_FMT_ARGS(id));
      }
    }
    sord_iter_free(iter);
    if (num_results != test.expected_num_results) {
      return test_fail("Fail: Expected %d results, got %d\n",
                       test.expected_num_results,
                       num_results);
    }
    fprintf(stderr, "OK (%i matches)\n", test.expected_num_results);
  }

  // Query blank node subject
  SordQuad pat = {sord_new_blank(world, USTR("ablank")), 0, 0};
  if (!pat[0]) {
    return test_fail("Blank node subject lost\n");
  }
  fprintf(stderr, "Query " TUP_FMT "... ", TUP_FMT_ARGS(pat));
  iter            = sord_find(sord, pat);
  int num_results = 0;
  for (; !sord_iter_end(iter); sord_iter_next(iter)) {
    sord_iter_get(iter, id);
    ++num_results;
    if (!sord_quad_match(pat, id)) {
      sord_iter_free(iter);
      return test_fail("Fail: Query result " TUP_FMT
                       " does not match pattern\n",
                       TUP_FMT_ARGS(id));
    }
  }
  fprintf(stderr, "OK\n");
  sord_node_free(world, (SordNode*)pat[0]);
  sord_iter_free(iter);
  if (num_results != 2) {
    return test_fail("Blank node subject query failed\n");
  }

  // Test nested queries
  fprintf(stderr, "Nested Queries... ");
  const SordNode* last_subject = 0;
  iter                         = sord_search(sord, NULL, NULL, NULL, NULL);
  for (; !sord_iter_end(iter); sord_iter_next(iter)) {
    sord_iter_get(iter, id);
    if (id[0] == last_subject) {
      continue;
    }

    SordQuad  subpat          = {id[0], 0, 0};
    SordIter* subiter         = sord_find(sord, subpat);
    unsigned  num_sub_results = 0;
    if (sord_iter_get_node(subiter, SORD_SUBJECT) != id[0]) {
      return test_fail("Fail: Incorrect initial submatch\n");
    }
    for (; !sord_iter_end(subiter); sord_iter_next(subiter)) {
      SordQuad subid;
      sord_iter_get(subiter, subid);
      if (!sord_quad_match(subpat, subid)) {
        sord_iter_free(iter);
        sord_iter_free(subiter);
        return test_fail("Fail: Nested query result does not match pattern\n");
      }
      ++num_sub_results;
    }
    sord_iter_free(subiter);
    if (num_sub_results != n_objects_per) {
      return test_fail("Fail: Nested query " TUP_FMT " failed"
                       " (%u results, expected %u)\n",
                       TUP_FMT_ARGS(subpat),
                       num_sub_results,
                       n_objects_per);
    }

    uint64_t count = sord_count(sord, id[0], 0, 0, 0);
    if (count != num_sub_results) {
      return test_fail("Fail: Query " TUP_FMT " sord_count() %" PRIu64
                       "does not match result count %u\n",
                       TUP_FMT_ARGS(subpat),
                       count,
                       num_sub_results);
    }

    last_subject = id[0];
  }
  fprintf(stderr, "OK\n\n");
  sord_iter_free(iter);

  return ret;
}

static SerdStatus
unexpected_error(void* handle, const SerdError* error)
{
  fprintf(stderr, "unexpected error: ");
  vfprintf(stderr, error->fmt, *error->args);
  return SERD_SUCCESS;
}

static SerdStatus
expected_error(void* handle, const SerdError* error)
{
  fprintf(stderr, "expected error: ");
  vfprintf(stderr, error->fmt, *error->args);
  ++n_expected_errors;
  return SERD_SUCCESS;
}

static int
finished(SordWorld* world, SordModel* sord, int status)
{
  sord_free(sord);
  sord_world_free(world);
  return status;
}

int
main(int argc, char** argv)
{
  static const size_t n_quads = 300;

  sord_free(NULL); // Shouldn't crash

  SordWorld* world = sord_world_new();

  // Attempt to create invalid URI
  fprintf(stderr, "expected ");
  SordNode* bad_uri = sord_new_uri(world, USTR("noscheme"));
  if (bad_uri) {
    return test_fail("Successfully created invalid URI \"noscheme\"\n");
  }
  sord_node_free(world, bad_uri);

  sord_world_set_error_sink(world, expected_error, NULL);

  // Attempt to create invalid CURIE
  SerdNode  base = serd_node_from_string(SERD_URI, USTR("http://example.org/"));
  SerdEnv*  env  = serd_env_new(&base);
  SerdNode  sbad = serd_node_from_string(SERD_CURIE, USTR("bad:"));
  SordNode* bad  = sord_node_from_serd_node(world, env, &sbad, NULL, NULL);
  if (bad) {
    return test_fail("Successfully created CURIE with bad namespace\n");
  }
  sord_node_free(world, bad);
  serd_env_free(env);

  // Attempt to create node from garbage
  SerdNode junk = SERD_NODE_NULL;
  junk.type     = (SerdType)1234;
  if (sord_node_from_serd_node(world, env, &junk, NULL, NULL)) {
    return test_fail("Successfully created node from garbage serd node\n");
  }

  // Attempt to create NULL node
  SordNode* nil_node =
    sord_node_from_serd_node(world, NULL, &SERD_NODE_NULL, NULL, NULL);
  if (nil_node) {
    return test_fail("Successfully created NULL node\n");
  }
  sord_node_free(world, nil_node);

  // Check node flags are set properly
  SordNode* with_newline = sord_new_literal(world, NULL, USTR("a\nb"), NULL);
  if (!(sord_node_get_flags(with_newline) & SERD_HAS_NEWLINE)) {
    return test_fail("Newline flag not set\n");
  }
  SordNode* with_quote = sord_new_literal(world, NULL, USTR("a\"b"), NULL);
  if (!(sord_node_get_flags(with_quote) & SERD_HAS_QUOTE)) {
    return test_fail("Quote flag not set\n");
  }

  // Create with minimal indexing
  SordModel* sord = sord_new(world, SORD_SPO, false);
  generate(world, sord, n_quads, NULL);

  if (test_read(world, sord, NULL, n_quads)) {
    sord_free(sord);
    sord_world_free(world);
    return EXIT_FAILURE;
  }

  // Check adding tuples with NULL fields fails
  sord_world_set_error_sink(world, expected_error, NULL);
  const size_t initial_num_quads = sord_num_quads(sord);
  SordQuad     tup               = {0, 0, 0, 0};
  if (sord_add(sord, tup)) {
    return test_fail("Added NULL tuple\n");
  }
  tup[0] = uri(world, 1);
  if (sord_add(sord, tup)) {
    return test_fail("Added tuple with NULL P and O\n");
  }
  tup[1] = uri(world, 2);
  if (sord_add(sord, tup)) {
    return test_fail("Added tuple with NULL O\n");
  }

  if (sord_num_quads(sord) != initial_num_quads) {
    return test_fail(
      "Num quads %zu != %zu\n", sord_num_quads(sord), initial_num_quads);
  }

  // Check adding tuples with an active iterator fails
  SordIter* iter = sord_begin(sord);
  tup[2]         = uri(world, 3);
  if (sord_add(sord, tup)) {
    return test_fail("Added tuple with active iterator\n");
  }

  // Check removing tuples with several active iterator fails
  SordIter* iter2 = sord_begin(sord);
  if (!sord_erase(sord, iter)) {
    return test_fail("Erased tuple with several active iterators\n");
  }
  n_expected_errors = 0;
  sord_remove(sord, tup);
  if (n_expected_errors != 1) {
    return test_fail("Removed tuple with several active iterators\n");
  }
  sord_iter_free(iter);
  sord_iter_free(iter2);

  sord_world_set_error_sink(world, unexpected_error, NULL);

  // Check interning merges equivalent values
  SordNode* uri_id   = sord_new_uri(world, USTR("http://example.org"));
  SordNode* blank_id = sord_new_blank(world, USTR("testblank"));
  SordNode* lit_id   = sord_new_literal(world, uri_id, USTR("hello"), NULL);
  if (sord_node_get_type(uri_id) != SORD_URI) {
    return test_fail("URI node has incorrect type\n");
  } else if (sord_node_get_type(blank_id) != SORD_BLANK) {
    return test_fail("Blank node has incorrect type\n");
  } else if (sord_node_get_type(lit_id) != SORD_LITERAL) {
    return test_fail("Literal node has incorrect type\n");
  }

  const size_t initial_num_nodes = sord_num_nodes(world);

  SordNode* uri_id2   = sord_new_uri(world, USTR("http://example.org"));
  SordNode* blank_id2 = sord_new_blank(world, USTR("testblank"));
  SordNode* lit_id2   = sord_new_literal(world, uri_id, USTR("hello"), NULL);
  if (uri_id2 != uri_id || !sord_node_equals(uri_id2, uri_id)) {
    fprintf(stderr, "Fail: URI interning failed (duplicates)\n");
    return finished(world, sord, EXIT_FAILURE);
  } else if (blank_id2 != blank_id || !sord_node_equals(blank_id2, blank_id)) {
    fprintf(stderr, "Fail: Blank node interning failed (duplicates)\n");
    return finished(world, sord, EXIT_FAILURE);
  } else if (lit_id2 != lit_id || !sord_node_equals(lit_id2, lit_id)) {
    fprintf(stderr, "Fail: Literal interning failed (duplicates)\n");
    return finished(world, sord, EXIT_FAILURE);
  }

  if (sord_num_nodes(world) != initial_num_nodes) {
    return test_fail(
      "Num nodes %zu != %zu\n", sord_num_nodes(world), initial_num_nodes);
  }

  const uint8_t ni_hao[] = {0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD, 0};
  SordNode*     chello   = sord_new_literal(world, NULL, ni_hao, "cmn");

  // Test literal length
  size_t         n_bytes;
  size_t         n_chars;
  const uint8_t* str = sord_node_get_string_counted(lit_id2, &n_bytes);
  if (strcmp((const char*)str, "hello")) {
    return test_fail("Literal node corrupt\n");
  } else if (n_bytes != strlen("hello")) {
    return test_fail("ASCII literal byte count incorrect\n");
  }

  str = sord_node_get_string_measured(lit_id2, &n_bytes, &n_chars);
  if (n_bytes != strlen("hello") || n_chars != strlen("hello")) {
    return test_fail("ASCII literal measured length incorrect\n");
  } else if (strcmp((const char*)str, "hello")) {
    return test_fail("ASCII literal string incorrect\n");
  }

  str = sord_node_get_string_measured(chello, &n_bytes, &n_chars);
  if (n_bytes != 6) {
    return test_fail("Multi-byte literal byte count incorrect\n");
  } else if (n_chars != 2) {
    return test_fail("Multi-byte literal character count incorrect\n");
  } else if (strcmp((const char*)str, (const char*)ni_hao)) {
    return test_fail("Multi-byte literal string incorrect\n");
  }

  // Check interning doesn't clash non-equivalent values
  SordNode* uri_id3   = sord_new_uri(world, USTR("http://example.orgX"));
  SordNode* blank_id3 = sord_new_blank(world, USTR("testblankX"));
  SordNode* lit_id3   = sord_new_literal(world, uri_id, USTR("helloX"), NULL);
  if (uri_id3 == uri_id || sord_node_equals(uri_id3, uri_id)) {
    fprintf(stderr, "Fail: URI interning failed (clash)\n");
    return finished(world, sord, EXIT_FAILURE);
  } else if (blank_id3 == blank_id || sord_node_equals(blank_id3, blank_id)) {
    fprintf(stderr, "Fail: Blank node interning failed (clash)\n");
    return finished(world, sord, EXIT_FAILURE);
  } else if (lit_id3 == lit_id || sord_node_equals(lit_id3, lit_id)) {
    fprintf(stderr, "Fail: Literal interning failed (clash)\n");
    return finished(world, sord, EXIT_FAILURE);
  }

  // Check literal interning
  SordNode* lit4 = sord_new_literal(world, NULL, USTR("hello"), NULL);
  SordNode* lit5 = sord_new_literal(world, uri_id2, USTR("hello"), NULL);
  SordNode* lit6 = sord_new_literal(world, NULL, USTR("hello"), "en-ca");
  if (lit4 == lit5 || sord_node_equals(lit4, lit5) || lit4 == lit6 ||
      sord_node_equals(lit4, lit6) || lit5 == lit6 ||
      sord_node_equals(lit5, lit6)) {
    fprintf(stderr, "Fail: Literal interning failed (type/lang clash)\n");
    return finished(world, sord, EXIT_FAILURE);
  }

  // Check relative URI construction
  SordNode* reluri =
    sord_new_relative_uri(world, USTR("a/b"), USTR("http://example.org/"));
  if (strcmp((const char*)sord_node_get_string(reluri),
             "http://example.org/a/b")) {
    fprintf(stderr,
            "Fail: Bad relative URI constructed: <%s>\n",
            sord_node_get_string(reluri));
    return finished(world, sord, EXIT_FAILURE);
  }
  SordNode* reluri2 = sord_new_relative_uri(
    world, USTR("http://drobilla.net/"), USTR("http://example.org/"));
  if (strcmp((const char*)sord_node_get_string(reluri2),
             "http://drobilla.net/")) {
    fprintf(stderr,
            "Fail: Bad relative URI constructed: <%s>\n",
            sord_node_get_string(reluri));
    return finished(world, sord, EXIT_FAILURE);
  }

  // Check comparison with NULL
  sord_node_free(world, uri_id);
  sord_node_free(world, blank_id);
  sord_node_free(world, lit_id);
  sord_node_free(world, uri_id2);
  sord_node_free(world, blank_id2);
  sord_node_free(world, lit_id2);
  sord_node_free(world, uri_id3);
  sord_node_free(world, blank_id3);
  sord_node_free(world, lit_id3);
  sord_free(sord);

  static const char* const index_names[6] = {
    "spo", "sop", "ops", "osp", "pso", "pos"};

  for (int i = 0; i < 6; ++i) {
    sord = sord_new(world, (1 << i), false);
    printf("Testing Index `%s'\n", index_names[i]);
    generate(world, sord, n_quads, 0);
    if (test_read(world, sord, 0, n_quads)) {
      return finished(world, sord, EXIT_FAILURE);
    }
    sord_free(sord);
  }

  static const char* const graph_index_names[6] = {
    "gspo", "gsop", "gops", "gosp", "gpso", "gpos"};

  for (int i = 0; i < 6; ++i) {
    sord = sord_new(world, (1 << i), true);
    printf("Testing Index `%s'\n", graph_index_names[i]);
    SordNode* graph = uri(world, 42);
    generate(world, sord, n_quads, graph);
    if (test_read(world, sord, graph, n_quads)) {
      return finished(world, sord, EXIT_FAILURE);
    }
    sord_free(sord);
  }

  // Test removing
  sord   = sord_new(world, SORD_SPO, true);
  tup[0] = uri(world, 1);
  tup[1] = uri(world, 2);
  tup[2] = sord_new_literal(world, 0, USTR("hello"), NULL);
  tup[3] = 0;
  sord_add(sord, tup);
  if (!sord_ask(sord, tup[0], tup[1], tup[2], tup[3])) {
    fprintf(stderr, "Failed to add tuple\n");
    return finished(world, sord, EXIT_FAILURE);
  }
  sord_node_free(world, (SordNode*)tup[2]);
  tup[2] = sord_new_literal(world, 0, USTR("hi"), NULL);
  sord_add(sord, tup);
  sord_remove(sord, tup);
  if (sord_num_quads(sord) != 1) {
    fprintf(
      stderr, "Remove failed (%zu quads, expected 1)\n", sord_num_quads(sord));
    return finished(world, sord, EXIT_FAILURE);
  }

  iter = sord_find(sord, tup);
  if (!sord_iter_end(iter)) {
    fprintf(stderr, "Found removed tuple\n");
    return finished(world, sord, EXIT_FAILURE);
  }
  sord_iter_free(iter);

  // Test double remove (silent success)
  sord_remove(sord, tup);

  // Load a couple graphs
  SordNode* graph42 = uri(world, 42);
  SordNode* graph43 = uri(world, 43);
  generate(world, sord, 1, graph42);
  generate(world, sord, 1, graph43);

  // Remove one graph via iterator
  SerdStatus st;
  iter = sord_search(sord, NULL, NULL, NULL, graph43);
  while (!sord_iter_end(iter)) {
    if ((st = sord_erase(sord, iter))) {
      fprintf(stderr, "Remove by iterator failed (%s)\n", serd_strerror(st));
      return finished(world, sord, EXIT_FAILURE);
    }
  }
  sord_iter_free(iter);

  // Erase the first tuple (an element in the default graph)
  iter = sord_begin(sord);
  if (sord_erase(sord, iter)) {
    return test_fail("Failed to erase begin iterator on non-empty model\n");
  }
  sord_iter_free(iter);

  // Ensure only the other graph is left
  SordQuad quad;
  SordQuad pat = {0, 0, 0, graph42};
  for (iter = sord_begin(sord); !sord_iter_end(iter); sord_iter_next(iter)) {
    sord_iter_get(iter, quad);
    if (!sord_quad_match(quad, pat)) {
      fprintf(stderr, "Graph removal via iteration failed\n");
      return finished(world, sord, EXIT_FAILURE);
    }
  }
  sord_iter_free(iter);

  // Load file into two separate graphs
  sord_free(sord);
  sord               = sord_new(world, SORD_SPO, true);
  env                = serd_env_new(&base);
  SordNode*   graph1 = sord_new_uri(world, USTR("http://example.org/graph1"));
  SordNode*   graph2 = sord_new_uri(world, USTR("http://example.org/graph2"));
  SerdReader* reader = sord_new_reader(sord, env, SERD_TURTLE, graph1);
  if ((st = serd_reader_read_string(reader, USTR("<s> <p> <o> .")))) {
    fprintf(stderr, "Failed to read string (%s)\n", serd_strerror(st));
    return finished(world, sord, EXIT_FAILURE);
  }
  serd_reader_free(reader);
  reader = sord_new_reader(sord, env, SERD_TURTLE, graph2);
  if ((st = serd_reader_read_string(reader, USTR("<s> <p> <o> .")))) {
    fprintf(stderr, "Failed to re-read string (%s)\n", serd_strerror(st));
    return finished(world, sord, EXIT_FAILURE);
  }
  serd_reader_free(reader);
  serd_env_free(env);

  // Ensure we only see triple once
  size_t n_triples = 0;
  for (iter = sord_begin(sord); !sord_iter_end(iter); sord_iter_next(iter)) {
    fprintf(stderr,
            "%s %s %s %s\n",
            sord_node_get_string(sord_iter_get_node(iter, SORD_SUBJECT)),
            sord_node_get_string(sord_iter_get_node(iter, SORD_PREDICATE)),
            sord_node_get_string(sord_iter_get_node(iter, SORD_OBJECT)),
            sord_node_get_string(sord_iter_get_node(iter, SORD_GRAPH)));

    ++n_triples;
  }
  sord_iter_free(iter);
  if (n_triples != 1) {
    fprintf(stderr, "Found duplicate triple\n");
    return finished(world, sord, EXIT_FAILURE);
  }

  // Test SPO iteration on an SOP indexed store
  sord_free(sord);
  sord = sord_new(world, SORD_SOP, false);
  generate(world, sord, 1, graph42);
  for (iter = sord_begin(sord); !sord_iter_end(iter); sord_iter_next(iter)) {
    ++n_triples;
  }
  sord_iter_free(iter);

  return finished(world, sord, EXIT_SUCCESS);
}
