/*
  Copyright 2007-2020 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.
*/

#undef NDEBUG

#include "lilv_test_uri_map.h"
#include "lilv_test_utils.h"

#include "../src/filesystem.h"

#include "lilv/lilv.h"
#include "lv2/core/lv2.h"
#include "lv2/state/state.h"
#include "lv2/urid/urid.h"
#include "serd/serd.h"

#ifdef _WIN32
#  include <direct.h>
#  define mkdir(path, flags) _mkdir(path)
#else
#  include <sys/stat.h>
#endif

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

#define TEST_PLUGIN_URI "http://example.org/lilv-test-plugin"

typedef struct {
  LilvTestEnv*        env;
  LilvTestUriMap      uri_map;
  LV2_URID_Map        map;
  LV2_Feature         map_feature;
  LV2_URID_Unmap      unmap;
  LV2_Feature         unmap_feature;
  LV2_State_Free_Path free_path;
  LV2_Feature         free_path_feature;
  const LV2_Feature*  features[4];
  LV2_URID            atom_Float;
  float               in;
  float               out;
  float               control;
} TestContext;

typedef struct {
  char* top;     ///< Temporary directory that contains everything
  char* shared;  ///< Common parent for shared directoryes (top/shared)
  char* scratch; ///< Scratch file directory (top/shared/scratch)
  char* copy;    ///< Copy directory (top/shared/scratch)
  char* link;    ///< Link directory (top/shared/scratch)
} TestDirectories;

static void
lilv_free_path(LV2_State_Free_Path_Handle handle, char* path)
{
  (void)handle;

  lilv_free(path);
}

static TestContext*
test_context_new(void)
{
  TestContext* ctx = (TestContext*)calloc(1, sizeof(TestContext));

  lilv_test_uri_map_init(&ctx->uri_map);

  ctx->env                    = lilv_test_env_new();
  ctx->map.handle             = &ctx->uri_map;
  ctx->map.map                = map_uri;
  ctx->map_feature.URI        = LV2_URID_MAP_URI;
  ctx->map_feature.data       = &ctx->map;
  ctx->unmap.handle           = &ctx->uri_map;
  ctx->unmap.unmap            = unmap_uri;
  ctx->unmap_feature.URI      = LV2_URID_UNMAP_URI;
  ctx->unmap_feature.data     = &ctx->unmap;
  ctx->free_path.free_path    = lilv_free_path;
  ctx->free_path_feature.URI  = LV2_STATE__freePath;
  ctx->free_path_feature.data = &ctx->free_path;
  ctx->features[0]            = &ctx->map_feature;
  ctx->features[1]            = &ctx->unmap_feature;
  ctx->features[2]            = &ctx->free_path_feature;
  ctx->features[3]            = NULL;

  ctx->atom_Float =
    map_uri(&ctx->uri_map, "http://lv2plug.in/ns/ext/atom#Float");

  ctx->in      = 1.0;
  ctx->out     = 42.0;
  ctx->control = 1234.0;

  return ctx;
}

static void
test_context_free(TestContext* ctx)
{
  lilv_test_uri_map_clear(&ctx->uri_map);
  lilv_test_env_free(ctx->env);
  free(ctx);
}

static TestDirectories
create_test_directories(void)
{
  TestDirectories dirs;

  char* const top = lilv_create_temporary_directory("lilv_XXXXXX");
  assert(top);

  /* On MacOS, temporary directories from mkdtemp involve symlinks, so
     resolve it here so that path comparisons in tests work. */

  dirs.top     = lilv_path_canonical(top);
  dirs.shared  = lilv_path_join(dirs.top, "shared");
  dirs.scratch = lilv_path_join(dirs.shared, "scratch");
  dirs.copy    = lilv_path_join(dirs.shared, "copy");
  dirs.link    = lilv_path_join(dirs.shared, "link");

  assert(!mkdir(dirs.shared, 0700));
  assert(!mkdir(dirs.scratch, 0700));
  assert(!mkdir(dirs.copy, 0700));
  assert(!mkdir(dirs.link, 0700));

  free(top);

  return dirs;
}

static TestDirectories
no_test_directories(void)
{
  TestDirectories dirs = {NULL, NULL, NULL, NULL, NULL};

  return dirs;
}

static void
remove_file(const char* path, const char* name, void* data)
{
  (void)data;

  char* const full_path = lilv_path_join(path, name);
  assert(!lilv_remove(full_path));
  free(full_path);
}

static void
cleanup_test_directories(const TestDirectories dirs)
{
  lilv_dir_for_each(dirs.scratch, NULL, remove_file);
  lilv_dir_for_each(dirs.copy, NULL, remove_file);
  lilv_dir_for_each(dirs.link, NULL, remove_file);

  assert(!lilv_remove(dirs.link));
  assert(!lilv_remove(dirs.copy));
  assert(!lilv_remove(dirs.scratch));
  assert(!lilv_remove(dirs.shared));
  assert(!lilv_remove(dirs.top));

  free(dirs.link);
  free(dirs.copy);
  free(dirs.scratch);
  free(dirs.shared);
  free(dirs.top);
}

static const void*
get_port_value(const char* port_symbol,
               void*       user_data,
               uint32_t*   size,
               uint32_t*   type)
{
  TestContext* ctx = (TestContext*)user_data;

  if (!strcmp(port_symbol, "input")) {
    *size = sizeof(float);
    *type = ctx->atom_Float;
    return &ctx->in;
  } else if (!strcmp(port_symbol, "output")) {
    *size = sizeof(float);
    *type = ctx->atom_Float;
    return &ctx->out;
  } else if (!strcmp(port_symbol, "control")) {
    *size = sizeof(float);
    *type = ctx->atom_Float;
    return &ctx->control;
  } else {
    fprintf(
      stderr, "error: get_port_value for nonexistent port `%s'\n", port_symbol);
    *size = *type = 0;
    return NULL;
  }
}

static void
set_port_value(const char* port_symbol,
               void*       user_data,
               const void* value,
               uint32_t    size,
               uint32_t    type)
{
  (void)size;
  (void)type;

  TestContext* ctx = (TestContext*)user_data;

  if (!strcmp(port_symbol, "input")) {
    ctx->in = *(const float*)value;
  } else if (!strcmp(port_symbol, "output")) {
    ctx->out = *(const float*)value;
  } else if (!strcmp(port_symbol, "control")) {
    ctx->control = *(const float*)value;
  } else {
    fprintf(
      stderr, "error: set_port_value for nonexistent port `%s'\n", port_symbol);
  }
}

static char*
make_scratch_path(LV2_State_Make_Path_Handle handle, const char* path)
{
  TestDirectories* dirs = (TestDirectories*)handle;

  return lilv_path_join(dirs->scratch, path);
}

static const LilvPlugin*
load_test_plugin(const TestContext* const ctx)
{
  LilvWorld* world      = ctx->env->world;
  uint8_t*   abs_bundle = (uint8_t*)lilv_path_absolute(LILV_TEST_BUNDLE);
  SerdNode   bundle     = serd_node_new_file_uri(abs_bundle, 0, 0, true);
  LilvNode*  bundle_uri = lilv_new_uri(world, (const char*)bundle.buf);
  LilvNode*  plugin_uri = lilv_new_uri(world, TEST_PLUGIN_URI);

  lilv_world_load_bundle(world, bundle_uri);

  const LilvPlugins* plugins = lilv_world_get_all_plugins(world);
  const LilvPlugin*  plugin  = lilv_plugins_get_by_uri(plugins, plugin_uri);

  lilv_node_free(plugin_uri);
  lilv_node_free(bundle_uri);
  serd_node_free(&bundle);
  free(abs_bundle);

  assert(plugin);
  return plugin;
}

static LilvState*
state_from_instance(const LilvPlugin* const      plugin,
                    LilvInstance* const          instance,
                    TestContext* const           ctx,
                    const TestDirectories* const dirs,
                    const char* const            bundle_path)
{
  return lilv_state_new_from_instance(plugin,
                                      instance,
                                      &ctx->map,
                                      dirs->scratch,
                                      dirs->copy,
                                      dirs->link,
                                      bundle_path,
                                      get_port_value,
                                      ctx,
                                      0,
                                      NULL);
}

static void
test_instance_state(void)
{
  TestContext* const      ctx    = test_context_new();
  const TestDirectories   dirs   = no_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);
  LilvInstance* const     instance =
    lilv_plugin_instantiate(plugin, 48000.0, ctx->features);

  assert(instance);

  // Get instance state
  LilvState* const state =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Check that state contains properties saved by the plugin
  assert(lilv_state_get_num_properties(state) == 8);

  // Check that state has no URI
  assert(!lilv_state_get_uri(state));

  // Check that state can't be saved without a URI
  assert(!lilv_state_to_string(
    ctx->env->world, &ctx->map, &ctx->unmap, state, NULL, NULL));

  // Check that we can't delete unsaved state
  assert(lilv_state_delete(ctx->env->world, state));

  lilv_state_free(state);
  lilv_instance_free(instance);
  test_context_free(ctx);
}

static void
test_equal(void)
{
  TestContext* const      ctx    = test_context_new();
  const TestDirectories   dirs   = no_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);
  LilvInstance* const     instance =
    lilv_plugin_instantiate(plugin, 48000.0, ctx->features);

  assert(instance);

  // Get instance state
  LilvState* const state_1 =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Get another instance state
  LilvState* const state_2 =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Ensure they are equal
  assert(lilv_state_equals(state_1, state_2));

  // Set a label on the second state
  assert(lilv_state_get_label(state_2) == NULL);
  lilv_state_set_label(state_2, "Test State Old Label");

  // Ensure they are no longer equal
  assert(!lilv_state_equals(state_1, state_2));

  lilv_state_free(state_2);
  lilv_state_free(state_1);
  lilv_instance_free(instance);
  test_context_free(ctx);
}

static void
test_changed_plugin_data(void)
{
  TestContext* const      ctx    = test_context_new();
  const TestDirectories   dirs   = no_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);
  LilvInstance* const     instance =
    lilv_plugin_instantiate(plugin, 48000.0, ctx->features);

  assert(instance);

  // Get initial state
  LilvState* const initial_state =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Run plugin to change internal state
  lilv_instance_activate(instance);
  lilv_instance_connect_port(instance, 0, &ctx->in);
  lilv_instance_connect_port(instance, 1, &ctx->out);
  lilv_instance_run(instance, 1);
  assert(ctx->in == 1.0);
  assert(ctx->out == 1.0);

  // Get a new instance state (which should now differ)
  LilvState* const changed_state =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Ensure state has changed
  assert(!lilv_state_equals(initial_state, changed_state));

  lilv_state_free(changed_state);
  lilv_state_free(initial_state);
  lilv_instance_free(instance);
  test_context_free(ctx);
}

static void
test_changed_metadata(void)
{
  TestContext* const      ctx    = test_context_new();
  const TestDirectories   dirs   = no_test_directories();
  LV2_URID_Map* const     map    = &ctx->map;
  const LilvPlugin* const plugin = load_test_plugin(ctx);
  LilvInstance* const     instance =
    lilv_plugin_instantiate(plugin, 48000.0, ctx->features);

  assert(instance);

  // Get initial state
  LilvState* const initial_state =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Save initial state to a string
  char* const initial_string =
    lilv_state_to_string(ctx->env->world,
                         &ctx->map,
                         &ctx->unmap,
                         initial_state,
                         "http://example.org/initial",
                         NULL);

  // Get another state
  LilvState* const changed_state =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Set a metadata property
  const LV2_URID key   = map->map(map->handle, "http://example.org/extra");
  const int32_t  value = 1;
  const LV2_URID type =
    map->map(map->handle, "http://lv2plug.in/ns/ext/atom#Int");
  lilv_state_set_metadata(changed_state,
                          key,
                          &value,
                          sizeof(value),
                          type,
                          LV2_STATE_IS_PORTABLE | LV2_STATE_IS_POD);

  // Save changed state to a string
  char* const changed_string =
    lilv_state_to_string(ctx->env->world,
                         &ctx->map,
                         &ctx->unmap,
                         changed_state,
                         "http://example.org/changed",
                         NULL);

  // Ensure that strings differ (metadata does not affect state equality)
  assert(strcmp(initial_string, changed_string));

  free(changed_string);
  lilv_state_free(changed_state);
  free(initial_string);
  lilv_state_free(initial_state);
  lilv_instance_free(instance);
  test_context_free(ctx);
}

static void
test_to_string(void)
{
  TestContext* const      ctx    = test_context_new();
  const TestDirectories   dirs   = no_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);
  LilvInstance* const     instance =
    lilv_plugin_instantiate(plugin, 48000.0, ctx->features);

  assert(instance);

  // Get initial state
  LilvState* const initial_state =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Run plugin to change internal state
  lilv_instance_activate(instance);
  lilv_instance_connect_port(instance, 0, &ctx->in);
  lilv_instance_connect_port(instance, 1, &ctx->out);
  lilv_instance_run(instance, 1);
  assert(ctx->in == 1.0);
  assert(ctx->out == 1.0);

  // Restore instance state to original state
  lilv_state_restore(initial_state, instance, set_port_value, ctx, 0, NULL);

  // Take a new snapshot of the state
  LilvState* const restored =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Check that new state matches the initial state
  assert(lilv_state_equals(initial_state, restored));

  lilv_state_free(restored);
  lilv_state_free(initial_state);
  lilv_instance_free(instance);
  test_context_free(ctx);
}

static void
test_string_round_trip(void)
{
  TestContext* const      ctx    = test_context_new();
  const TestDirectories   dirs   = no_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);
  LilvInstance* const     instance =
    lilv_plugin_instantiate(plugin, 48000.0, ctx->features);

  assert(instance);

  // Get initial state
  LilvState* const initial_state =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Save state to a string
  char* const string = lilv_state_to_string(ctx->env->world,
                                            &ctx->map,
                                            &ctx->unmap,
                                            initial_state,
                                            "http://example.org/string",
                                            NULL);

  // Restore from string
  LilvState* const restored =
    lilv_state_new_from_string(ctx->env->world, &ctx->map, string);

  // Ensure they are equal
  assert(lilv_state_equals(initial_state, restored));

  // Check that the restored state refers to the correct plugin
  const LilvNode* state_plugin_uri = lilv_state_get_plugin_uri(restored);
  assert(!strcmp(lilv_node_as_string(state_plugin_uri), TEST_PLUGIN_URI));

  lilv_state_free(restored);
  free(string);
  lilv_state_free(initial_state);
  lilv_instance_free(instance);
  test_context_free(ctx);
}

static SerdStatus
count_sink(void* const              handle,
           const SerdStatementFlags flags,
           const SerdNode* const    graph,
           const SerdNode* const    subject,
           const SerdNode* const    predicate,
           const SerdNode* const    object,
           const SerdNode* const    object_datatype,
           const SerdNode* const    object_lang)
{
  (void)flags;
  (void)graph;
  (void)subject;
  (void)predicate;
  (void)object;
  (void)object_datatype;
  (void)object_lang;

  size_t* const n_statements = (size_t*)handle;

  ++(*n_statements);

  return SERD_SUCCESS;
}

static size_t
count_statements(const char* path)
{
  size_t n_statements = 0;

  SerdReader* reader = serd_reader_new(
    SERD_TURTLE, &n_statements, NULL, NULL, NULL, count_sink, NULL);

  SerdNode uri = serd_node_new_file_uri((const uint8_t*)path, NULL, NULL, true);

  assert(!serd_reader_read_file(reader, uri.buf));

  serd_node_free(&uri);
  serd_reader_free(reader);

  return n_statements;
}

static void
test_to_files(void)
{
  TestContext* const      ctx    = test_context_new();
  TestDirectories         dirs   = create_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);

  LV2_State_Make_Path make_path         = {&dirs, make_scratch_path};
  LV2_Feature         make_path_feature = {LV2_STATE__makePath, &make_path};

  const LV2_Feature* const instance_features[] = {
    &ctx->map_feature, &ctx->free_path_feature, &make_path_feature, NULL};

  LilvInstance* const instance =
    lilv_plugin_instantiate(plugin, 48000.0, instance_features);

  assert(instance);

  // Run plugin to generate some recording file data
  lilv_instance_activate(instance);
  lilv_instance_connect_port(instance, 0, &ctx->in);
  lilv_instance_connect_port(instance, 1, &ctx->out);
  lilv_instance_run(instance, 1);
  lilv_instance_run(instance, 2);
  assert(ctx->in == 1.0);
  assert(ctx->out == 1.0);

  // Check that the test plugin has made its recording scratch file
  char* const recfile_path = lilv_path_join(dirs.scratch, "recfile");
  assert(lilv_path_exists(recfile_path));

  // Get state
  char* const      bundle_1_path = lilv_path_join(dirs.top, "state1.lv2");
  LilvState* const state_1 =
    state_from_instance(plugin, instance, ctx, &dirs, bundle_1_path);

  // Check that state contains properties saved by the plugin (with files)
  assert(lilv_state_get_num_properties(state_1) == 10);

  // Check that a snapshop of the recfile was created
  char* const recfile_copy_1 = lilv_path_join(dirs.copy, "recfile");
  assert(lilv_path_exists(recfile_copy_1));

  // Save state to a bundle
  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          state_1,
                          "http://example.org/state1",
                          bundle_1_path,
                          "state.ttl"));

  // Check that a manifest exists
  char* const manifest_path = lilv_path_join(bundle_1_path, "manifest.ttl");
  assert(lilv_path_exists(manifest_path));

  // Check that the expected statements are in the manifest file
  assert(count_statements(manifest_path) == 3);

  // Check that a link to the recfile exists in the saved bundle
  char* const recfile_link_1 = lilv_path_join(bundle_1_path, "recfile");
  assert(lilv_path_exists(recfile_link_1));

  // Check that link points to the corresponding copy
  assert(lilv_file_equals(recfile_link_1, recfile_copy_1));

  // Run plugin again to modify recording file data
  lilv_instance_run(instance, 2);

  // Get updated state
  char* const      bundle_2_path = lilv_path_join(dirs.top, "state2.lv2");
  LilvState* const state_2 =
    state_from_instance(plugin, instance, ctx, &dirs, bundle_2_path);

  // Save updated state to a bundle
  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          state_2,
                          NULL,
                          bundle_2_path,
                          "state.ttl"));

  // Check that a new snapshop of the recfile was created
  char* const recfile_copy_2 = lilv_path_join(dirs.copy, "recfile.2");
  assert(lilv_path_exists(recfile_copy_2));

  // Check that a link to the recfile exists in the updated bundle
  char* const recfile_link_2 = lilv_path_join(bundle_2_path, "recfile");
  assert(lilv_path_exists(recfile_link_2));

  // Check that link points to the corresponding copy
  assert(lilv_file_equals(recfile_link_2, recfile_copy_2));

  lilv_instance_free(instance);
  lilv_dir_for_each(bundle_2_path, NULL, remove_file);
  lilv_dir_for_each(bundle_1_path, NULL, remove_file);
  assert(!lilv_remove(bundle_2_path));
  assert(!lilv_remove(bundle_1_path));
  cleanup_test_directories(dirs);

  free(recfile_link_2);
  free(recfile_copy_2);
  lilv_state_free(state_2);
  free(bundle_2_path);
  free(recfile_link_1);
  free(manifest_path);
  free(recfile_copy_1);
  lilv_state_free(state_1);
  free(bundle_1_path);
  free(recfile_path);
  test_context_free(ctx);
}

static void
test_multi_save(void)
{
  TestContext* const      ctx    = test_context_new();
  TestDirectories         dirs   = create_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);

  LV2_State_Make_Path make_path         = {&dirs, make_scratch_path};
  LV2_Feature         make_path_feature = {LV2_STATE__makePath, &make_path};

  const LV2_Feature* const instance_features[] = {
    &ctx->map_feature, &ctx->free_path_feature, &make_path_feature, NULL};

  LilvInstance* const instance =
    lilv_plugin_instantiate(plugin, 48000.0, instance_features);

  assert(instance);

  // Get state
  char* const      bundle_1_path = lilv_path_join(dirs.top, "state1.lv2");
  LilvState* const state_1 =
    state_from_instance(plugin, instance, ctx, &dirs, bundle_1_path);

  // Save state to a bundle
  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          state_1,
                          "http://example.org/state1",
                          bundle_1_path,
                          "state.ttl"));

  // Check that a manifest exists
  char* const manifest_path = lilv_path_join(bundle_1_path, "manifest.ttl");
  assert(lilv_path_exists(manifest_path));

  // Check that the state file exists
  char* const state_path = lilv_path_join(bundle_1_path, "state.ttl");
  assert(lilv_path_exists(state_path));

  // Check that the expected statements are in the files
  assert(count_statements(manifest_path) == 3);
  assert(count_statements(state_path) == 21);

  // Save state again to the same bundle
  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          state_1,
                          "http://example.org/state1",
                          bundle_1_path,
                          "state.ttl"));

  // Check that everything is the same
  assert(lilv_path_exists(manifest_path));
  assert(lilv_path_exists(state_path));
  assert(count_statements(manifest_path) == 3);
  assert(count_statements(state_path) == 21);

  lilv_instance_free(instance);
  lilv_dir_for_each(bundle_1_path, NULL, remove_file);
  lilv_remove(bundle_1_path);
  cleanup_test_directories(dirs);

  free(state_path);
  free(manifest_path);
  lilv_state_free(state_1);
  free(bundle_1_path);
  test_context_free(ctx);
}

static void
test_files_round_trip(void)
{
  TestContext* const      ctx    = test_context_new();
  TestDirectories         dirs   = create_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);

  LV2_State_Make_Path make_path         = {&dirs, make_scratch_path};
  LV2_Feature         make_path_feature = {LV2_STATE__makePath, &make_path};

  const LV2_Feature* const instance_features[] = {
    &ctx->map_feature, &ctx->free_path_feature, &make_path_feature, NULL};

  LilvInstance* const instance =
    lilv_plugin_instantiate(plugin, 48000.0, instance_features);

  assert(instance);

  // Run plugin to generate some recording file data
  lilv_instance_activate(instance);
  lilv_instance_connect_port(instance, 0, &ctx->in);
  lilv_instance_connect_port(instance, 1, &ctx->out);
  lilv_instance_run(instance, 1);
  lilv_instance_run(instance, 2);
  assert(ctx->in == 1.0);
  assert(ctx->out == 1.0);

  // Save first state to a bundle
  char* const      bundle_1_1_path = lilv_path_join(dirs.top, "state1_1.lv2");
  LilvState* const state_1_1 =
    state_from_instance(plugin, instance, ctx, &dirs, bundle_1_1_path);

  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          state_1_1,
                          NULL,
                          bundle_1_1_path,
                          "state.ttl"));

  // Save first state to another bundle
  char* const      bundle_1_2_path = lilv_path_join(dirs.top, "state1_2.lv2");
  LilvState* const state_1_2 =
    state_from_instance(plugin, instance, ctx, &dirs, bundle_1_2_path);

  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          state_1_2,
                          NULL,
                          bundle_1_2_path,
                          "state.ttl"));

  // Load both first state bundles and check that the results are equal
  char* const state_1_1_path = lilv_path_join(bundle_1_1_path, "state.ttl");
  char* const state_1_2_path = lilv_path_join(bundle_1_2_path, "state.ttl");

  LilvState* state_1_1_loaded =
    lilv_state_new_from_file(ctx->env->world, &ctx->map, NULL, state_1_1_path);

  LilvState* state_1_2_loaded =
    lilv_state_new_from_file(ctx->env->world, &ctx->map, NULL, state_1_2_path);

  assert(state_1_1_loaded);
  assert(state_1_2_loaded);
  assert(lilv_state_equals(state_1_1_loaded, state_1_2_loaded));

  // Run plugin again to modify recording file data
  lilv_instance_run(instance, 2);

  // Save updated state to a bundle
  char* const      bundle_2_path = lilv_path_join(dirs.top, "state2.lv2");
  LilvState* const state_2 =
    state_from_instance(plugin, instance, ctx, &dirs, bundle_2_path);

  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          state_2,
                          NULL,
                          bundle_2_path,
                          "state.ttl"));

  // Load updated state bundle and check that it differs from the others
  char* const state_2_path = lilv_path_join(bundle_2_path, "state.ttl");

  LilvState* state_2_loaded =
    lilv_state_new_from_file(ctx->env->world, &ctx->map, NULL, state_2_path);

  assert(state_2_loaded);
  assert(!lilv_state_equals(state_1_1_loaded, state_2_loaded));

  lilv_instance_free(instance);
  lilv_dir_for_each(bundle_1_1_path, NULL, remove_file);
  lilv_dir_for_each(bundle_1_2_path, NULL, remove_file);
  lilv_dir_for_each(bundle_2_path, NULL, remove_file);
  lilv_remove(bundle_1_1_path);
  lilv_remove(bundle_1_2_path);
  lilv_remove(bundle_2_path);
  cleanup_test_directories(dirs);

  lilv_state_free(state_2_loaded);
  free(state_2_path);
  lilv_state_free(state_2);
  free(bundle_2_path);
  lilv_state_free(state_1_2_loaded);
  lilv_state_free(state_1_1_loaded);
  free(state_1_2_path);
  free(state_1_1_path);
  lilv_state_free(state_1_2);
  free(bundle_1_2_path);
  lilv_state_free(state_1_1);
  free(bundle_1_1_path);
  test_context_free(ctx);
}

static void
test_world_round_trip(void)
{
  TestContext* const       ctx       = test_context_new();
  LilvWorld* const         world     = ctx->env->world;
  TestDirectories          dirs      = create_test_directories();
  const LilvPlugin* const  plugin    = load_test_plugin(ctx);
  static const char* const state_uri = "http://example.org/worldState";

  LV2_State_Make_Path make_path         = {&dirs, make_scratch_path};
  LV2_Feature         make_path_feature = {LV2_STATE__makePath, &make_path};

  const LV2_Feature* const instance_features[] = {
    &ctx->map_feature, &ctx->free_path_feature, &make_path_feature, NULL};

  LilvInstance* const instance =
    lilv_plugin_instantiate(plugin, 48000.0, instance_features);

  assert(instance);

  // Run plugin to generate some recording file data
  lilv_instance_activate(instance);
  lilv_instance_connect_port(instance, 0, &ctx->in);
  lilv_instance_connect_port(instance, 1, &ctx->out);
  lilv_instance_run(instance, 1);
  lilv_instance_run(instance, 2);
  assert(ctx->in == 1.0);
  assert(ctx->out == 1.0);

  // Save state to a bundle
  char* const      bundle_path = lilv_path_join(dirs.top, "state.lv2/");
  LilvState* const start_state =
    state_from_instance(plugin, instance, ctx, &dirs, bundle_path);

  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          start_state,
                          state_uri,
                          bundle_path,
                          "state.ttl"));

  // Load state bundle into world
  SerdNode bundle_uri =
    serd_node_new_file_uri((const uint8_t*)bundle_path, 0, 0, true);
  LilvNode* const bundle_node =
    lilv_new_uri(world, (const char*)bundle_uri.buf);
  LilvNode* const state_node = lilv_new_uri(world, state_uri);
  lilv_world_load_bundle(world, bundle_node);
  lilv_world_load_resource(world, state_node);

  // Ensure the state loaded from the world matches
  LilvState* const restored =
    lilv_state_new_from_world(world, &ctx->map, state_node);
  assert(lilv_state_equals(start_state, restored));

  // Unload state from world
  lilv_world_unload_resource(world, state_node);
  lilv_world_unload_bundle(world, bundle_node);

  // Ensure that it is no longer present
  assert(!lilv_state_new_from_world(world, &ctx->map, state_node));

  lilv_instance_free(instance);
  lilv_state_delete(world, start_state);
  cleanup_test_directories(dirs);

  lilv_state_free(restored);
  lilv_node_free(state_node);
  lilv_node_free(bundle_node);
  serd_node_free(&bundle_uri);
  lilv_state_free(start_state);
  free(bundle_path);
  test_context_free(ctx);
}

static void
test_label_round_trip(void)
{
  TestContext* const      ctx    = test_context_new();
  const TestDirectories   dirs   = create_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);
  LilvInstance* const     instance =
    lilv_plugin_instantiate(plugin, 48000.0, ctx->features);

  assert(instance);

  // Get initial state
  LilvState* const state =
    state_from_instance(plugin, instance, ctx, &dirs, NULL);

  // Set a label
  lilv_state_set_label(state, "Monopoly on violence");

  // Save to a bundle
  char* const bundle_path = lilv_path_join(dirs.top, "state.lv2/");
  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          state,
                          NULL,
                          bundle_path,
                          "state.ttl"));

  // Load bundle and check the label and that the states are equal
  char* const state_path = lilv_path_join(bundle_path, "state.ttl");

  LilvState* const loaded =
    lilv_state_new_from_file(ctx->env->world, &ctx->map, NULL, state_path);

  assert(loaded);
  assert(lilv_state_equals(state, loaded));
  assert(!strcmp(lilv_state_get_label(loaded), "Monopoly on violence"));

  lilv_instance_free(instance);
  lilv_state_delete(ctx->env->world, state);
  cleanup_test_directories(dirs);

  lilv_state_free(loaded);
  free(state_path);
  free(bundle_path);
  lilv_state_free(state);
  test_context_free(ctx);
}

static void
test_bad_subject(void)
{
  TestContext* const ctx    = test_context_new();
  LilvNode* const    string = lilv_new_string(ctx->env->world, "Not a URI");

  LilvState* const file_state = lilv_state_new_from_file(
    ctx->env->world, &ctx->map, string, "/I/do/not/matter");

  assert(!file_state);

  LilvState* const world_state =
    lilv_state_new_from_world(ctx->env->world, &ctx->map, string);

  assert(!world_state);

  lilv_node_free(string);
  test_context_free(ctx);
}

static void
count_file(const char* path, const char* name, void* data)
{
  (void)path;
  (void)name;

  *(unsigned*)data += 1;
}

static void
test_delete(void)
{
  TestContext* const      ctx    = test_context_new();
  TestDirectories         dirs   = create_test_directories();
  const LilvPlugin* const plugin = load_test_plugin(ctx);

  LV2_State_Make_Path make_path         = {&dirs, make_scratch_path};
  LV2_Feature         make_path_feature = {LV2_STATE__makePath, &make_path};

  const LV2_Feature* const instance_features[] = {
    &ctx->map_feature, &ctx->free_path_feature, &make_path_feature, NULL};

  LilvInstance* const instance =
    lilv_plugin_instantiate(plugin, 48000.0, instance_features);

  assert(instance);

  // Run plugin to generate some recording file data
  lilv_instance_activate(instance);
  lilv_instance_connect_port(instance, 0, &ctx->in);
  lilv_instance_connect_port(instance, 1, &ctx->out);
  lilv_instance_run(instance, 1);
  lilv_instance_run(instance, 2);
  assert(ctx->in == 1.0);
  assert(ctx->out == 1.0);

  // Save state to a bundle
  char* const      bundle_path = lilv_path_join(dirs.top, "state.lv2/");
  LilvState* const state =
    state_from_instance(plugin, instance, ctx, &dirs, bundle_path);

  assert(!lilv_state_save(ctx->env->world,
                          &ctx->map,
                          &ctx->unmap,
                          state,
                          NULL,
                          bundle_path,
                          "state.ttl"));

  // Count the number of shared files before doing anything
  unsigned n_shared_files_before = 0;
  lilv_dir_for_each(dirs.shared, &n_shared_files_before, count_file);

  lilv_instance_free(instance);

  // Delete the state
  assert(!lilv_state_delete(ctx->env->world, state));

  // Ensure the number of shared files is the same after deletion
  unsigned n_shared_files_after = 0;
  lilv_dir_for_each(dirs.shared, &n_shared_files_after, count_file);
  assert(n_shared_files_before == n_shared_files_after);

  // Ensure the state directory has been deleted
  assert(!lilv_path_exists(bundle_path));

  cleanup_test_directories(dirs);

  lilv_state_free(state);
  free(bundle_path);
  test_context_free(ctx);
}

int
main(void)
{
  test_instance_state();
  test_equal();
  test_changed_plugin_data();
  test_changed_metadata();
  test_to_string();
  test_string_round_trip();
  test_to_files();
  test_multi_save();
  test_files_round_trip();
  test_world_round_trip();
  test_label_round_trip();
  test_bad_subject();
  test_delete();

  return 0;
}
