/**
 * Looking Glass
 * Copyright © 2017-2026 The Looking Glass Authors
 * https://looking-glass.io
 *
 * This program 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 2 of the License, or (at your option)
 * any later version.
 *
 * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "core/clipboard.h"
#include "core/clipboard_files.h"
#include "test.h"

#include "common/debug.h"
#include "common/KVMFRClipboard.h"

#include <fcntl.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

static unsigned notices;
static uint64_t datasets[16];
static unsigned acquireCount;
static uint64_t acquiredDataset;
static uint64_t acquiredRequest;
static unsigned releaseCount;
static uint64_t releasedDataset;
static uint64_t releasedAcquisition;
static unsigned requestCount;
static LG_ClipboardFileRequest remoteRequest;
static unsigned remoteReadyCount;
static unsigned remoteFailedCount;
static uint64_t remoteFailedDataset;
static bool remoteRequestSucceeds = true;
static uint8_t * responseData;
static size_t responseSize;
static size_t responseCapacity;
static bool responseOpen;
static bool responseComplete;
static bool responseWireEnded;
static unsigned cancelCount;
static uint64_t cancelledDataset;
static uint64_t cancelledRequest;
static LG_ClipboardFileError cancelledReason;

void clipboard_notifyFiles(uint64_t dataset)
{
  CHECK(notices < sizeof(datasets) / sizeof(datasets[0]));
  datasets[notices++] = dataset;
}

bool clipboard_fileAcquire(uint64_t dataset, uint64_t acquisition)
{
  ++acquireCount;
  acquiredDataset = dataset;
  acquiredRequest = acquisition;
  return true;
}

bool clipboard_fileAcquired(uint64_t dataset, uint64_t acquisition,
    LG_ClipboardFileError error)
{
  (void)dataset;
  (void)acquisition;
  (void)error;
  return false;
}

bool clipboard_fileRelease(uint64_t dataset, uint64_t acquisition)
{
  ++releaseCount;
  releasedDataset = dataset;
  releasedAcquisition = acquisition;
  return true;
}

bool clipboard_fileRequest(const LG_ClipboardFileRequest * request)
{
  CHECK(request);
  ++requestCount;
  remoteRequest = *request;
  return remoteRequestSucceeds;
}

LG_ClipboardResult clipboard_fileDataBegin(
    const LG_ClipboardFileRequest * request, uint64_t sizeHint)
{
  CHECK(request);
  (void)sizeHint;
  responseSize      = 0;
  responseOpen      = true;
  responseComplete  = false;
  responseWireEnded = false;
  return LG_CLIPBOARD_RESULT_ACCEPTED;
}

LG_ClipboardResult clipboard_fileDataChunk(
    const LG_ClipboardFileRequest * request, uint64_t responseOffset,
    const void * data, size_t size, bool end)
{
  CHECK(request);
  CHECK(responseOpen);
  CHECK(responseOffset == responseSize);
  CHECK(data || !size);
  CHECK(size <= SIZE_MAX - responseSize);
  const size_t wanted = responseSize + size;
  if (wanted > responseCapacity)
  {
    uint8_t * resized = realloc(responseData, wanted);
    CHECK(resized);
    responseData     = resized;
    responseCapacity = wanted;
  }
  if (size)
    memcpy(responseData + responseSize, data, size);
  responseSize = wanted;
  if (end)
    responseWireEnded = true;
  return LG_CLIPBOARD_RESULT_ACCEPTED;
}

LG_ClipboardResult clipboard_fileDataEnd(
    const LG_ClipboardFileRequest * request, uint64_t finalSize)
{
  CHECK(request);
  CHECK(responseOpen);
  CHECK(finalSize == responseSize);
  CHECK(!finalSize || responseWireEnded);
  responseOpen     = false;
  responseComplete = true;
  return LG_CLIPBOARD_RESULT_ACCEPTED;
}

bool clipboard_fileCancel(uint64_t dataset, uint64_t request,
    LG_ClipboardFileError reason)
{
  ++cancelCount;
  cancelledDataset = dataset;
  cancelledRequest = request;
  cancelledReason = reason;
  return true;
}

void clipboard_fileRemoteReady(uint64_t dataset)
{
  CHECK(dataset == acquiredDataset);
  ++remoteReadyCount;
}

void clipboard_fileRemoteFailed(uint64_t dataset)
{
  CHECK(dataset);
  ++remoteFailedCount;
  remoteFailedDataset = dataset;
}

static bool runLocalRequest(uint64_t dataset, uint64_t node,
    LG_ClipboardFileOperation operation, uint64_t offset, uint32_t length)
{
  static uint64_t requestId = 1;
  responseSize       = 0;
  responseOpen       = false;
  responseComplete   = false;
  responseWireEnded  = false;
  cancelCount        = 0;
  cancelledDataset   = 0;
  cancelledRequest   = 0;
  cancelledReason    = LG_CLIPBOARD_FILE_ERROR_NONE;
  const LG_ClipboardFileRequest request =
  {
    .dataset   = dataset,
    .request   = requestId++,
    .node      = node,
    .offset    = offset,
    .length    = length,
    .operation = operation,
  };
  const bool result = clipboardFiles_testLocalRequest(&request);
  CHECK(!result || responseComplete);
  CHECK(!result || !responseSize || responseWireEnded);
  CHECK(result ? cancelCount == 0U : cancelCount == 1U);
  if (!result)
  {
    CHECK(cancelledDataset == dataset);
    CHECK(cancelledRequest == request.request);
    CHECK(cancelledReason != LG_CLIPBOARD_FILE_ERROR_NONE);
  }
  return result;
}

static bool responseEntry(const char * name, KVMFRClipboardFileEntry * result)
{
  size_t offset = 0;
  while (offset < responseSize)
  {
    CHECK(responseSize - offset >= sizeof(*result));
    KVMFRClipboardFileEntry entry;
    memcpy(&entry, responseData + offset, sizeof(entry));
    const uint64_t bytes = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(
        entry.nameLength);
    CHECK(bytes <= SIZE_MAX);
    CHECK((size_t)bytes <= responseSize - offset);
    const char * entryName = (const char *)responseData + offset +
      sizeof(entry);
    if (strlen(name) == entry.nameLength &&
        !memcmp(entryName, name, entry.nameLength))
    {
      *result = entry;
      return true;
    }
    offset += (size_t)bytes;
  }
  CHECK(offset == responseSize);
  return false;
}

static void createFile(const char * path, const char * data)
{
  const int fd = open(path,
      O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600);
  CHECK(fd >= 0);
  const size_t length = strlen(data);
  CHECK(write(fd, data, length) == (ssize_t)length);
  CHECK(close(fd) == 0);
}

static void testLocalLifecycle(void)
{
  char directory[] = "/tmp/lg-clipboard-files-XXXXXX";
  CHECK(mkdtemp(directory));
  char path[256];
  CHECK(snprintf(path, sizeof(path), "%s/item.txt", directory) > 0);
  const int fd = open(path, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600);
  CHECK(fd >= 0);
  CHECK(write(fd, "clipboard", 9) == 9);
  CHECK(close(fd) == 0);

  CHECK(clipboardFiles_testInit(UINT64_C(0x1234000012340000)));
  char uri[320];
  const int uriLength = snprintf(uri, sizeof(uri), "file://%s\r\n", path);
  CHECK(uriLength > 0 && (size_t)uriLength < sizeof(uri));
  CHECK(clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)uriLength));
  CHECK(notices == 1);
  CHECK(datasets[0] != 0);
  CHECK(kvmfrClipboardTransferFromClient(datasets[0]));

  clipboardFiles_clearLocal();
  char gnome[336];
  const int gnomeLength = snprintf(
      gnome, sizeof(gnome), "copy\nfile://%s\n", path);
  CHECK(gnomeLength > 0 && (size_t)gnomeLength < sizeof(gnome));
  CHECK(clipboardFiles_setLocal("x-special/gnome-copied-files",
        gnome, (size_t)gnomeLength));
  CHECK(notices == 2);
  CHECK(datasets[1] != datasets[0]);

  clipboardFiles_clearLocal();
  CHECK(clipboardFiles_setLocal("x-special/mate-copied-files",
        gnome, (size_t)gnomeLength));
  CHECK(notices == 3);
  CHECK(datasets[2] != datasets[0]);
  CHECK(datasets[2] != datasets[1]);

  static const char invalid[] = "file:///does/not/exist\n";
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", invalid, sizeof(invalid) - 1U));
  clipboardFiles_clearLocal();
  clipboardFiles_free();
  clipboardFiles_free();

  CHECK(clipboardFiles_testInit(UINT64_C(0x5678000056780000)));
  CHECK(clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)uriLength));
  CHECK(notices == 4);
  CHECK(datasets[3] != datasets[0]);
  CHECK(datasets[3] != datasets[1]);
  CHECK(datasets[3] != datasets[2]);
  clipboardFiles_free();

  CHECK(unlink(path) == 0);
  CHECK(rmdir(directory) == 0);
}

static void testLocalUriValidation(void)
{
  char directory[] = "/tmp/lg-clipboard-uri-XXXXXX";
  CHECK(mkdtemp(directory));

  char plain[256];
  char spaced[256];
  char link[256];
  char alias[256];
  CHECK(snprintf(plain, sizeof(plain), "%s/item.txt", directory) > 0);
  CHECK(snprintf(spaced, sizeof(spaced),
        "%s/item space.txt", directory) > 0);
  CHECK(snprintf(link, sizeof(link), "%s/link.txt", directory) > 0);
  CHECK(snprintf(alias, sizeof(alias), "%s/alias", directory) > 0);
  createFile(plain, "plain");
  createFile(spaced, "spaced");
  CHECK(symlink("item.txt", link) == 0);
  CHECK(symlink(".", alias) == 0);

  CHECK(clipboardFiles_testInit(UINT64_C(0x684b2452684b2452)));
  const unsigned before = notices;
  char uri[512];
  int length = snprintf(uri, sizeof(uri),
      "FILE://LOCALHOST%s/item%%20space.txt\r\n", directory);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  clipboardFiles_clearLocal();

  length = snprintf(uri, sizeof(uri), "file://%s%%\n", plain);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri), "file://%s%%0\n", plain);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri), "file://%s%%GG\n", plain);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri), "file://%s%%00suffix\n", plain);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri), "file://%s?query\n", plain);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri), "file://%s#fragment\n", plain);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri), "file://example.invalid%s\n", plain);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  static const char root[] = "file:///\n";
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", root, sizeof(root) - 1U));
  length = snprintf(uri, sizeof(uri), "file://%s/\n", directory);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  clipboardFiles_clearLocal();
  length = snprintf(uri, sizeof(uri), "file://%s/./item.txt\n", directory);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri), "file://%s//item.txt\n", directory);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri), "file://%s\n", link);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri),
      "file://%s/alias/item.txt\n", directory);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri), "file://%s\n", spaced);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)length));
  length = snprintf(uri, sizeof(uri),
      "cutx\nfile://%s\n", plain);
  CHECK(length > 0 && (size_t)length < sizeof(uri));
  CHECK(!clipboardFiles_setLocal(
        "x-special/gnome-copied-files", uri, (size_t)length));
  CHECK(!clipboardFiles_setLocal(
        "x-special/mate-copied-files", uri, (size_t)length));
  CHECK(notices == before + 2U);

  clipboardFiles_free();
  CHECK(unlink(alias) == 0);
  CHECK(unlink(link) == 0);
  CHECK(unlink(spaced) == 0);
  CHECK(unlink(plain) == 0);
  CHECK(rmdir(directory) == 0);
}

static void testLocalSnapshot(void)
{
  char directory[] = "/tmp/lg-clipboard-snapshot-XXXXXX";
  CHECK(mkdtemp(directory));
  char stable[256];
  char changed[256];
  CHECK(snprintf(stable, sizeof(stable),
        "%s/stable.txt", directory) > 0);
  CHECK(snprintf(changed, sizeof(changed),
        "%s/changed.txt", directory) > 0);
  createFile(stable, "stable");
  createFile(changed, "before");

  CHECK(clipboardFiles_testInit(UINT64_C(0x1a72021b1a72021b)));
  char uri[320];
  const int uriLength = snprintf(
      uri, sizeof(uri), "file://%s\r\n", directory);
  CHECK(uriLength > 0 && (size_t)uriLength < sizeof(uri));
  CHECK(clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)uriLength));
  const uint64_t dataset = datasets[notices - 1U];

  CHECK(runLocalRequest(dataset, KVMFR_CLIPBOARD_FILE_ROOT_NODE,
        LG_CLIPBOARD_FILE_LIST, 0, 0));
  const char * base = strrchr(directory, '/');
  CHECK(base && base[1]);
  KVMFRClipboardFileEntry rootEntry;
  CHECK(responseEntry(base + 1, &rootEntry));
  CHECK(rootEntry.type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY);

  CHECK(runLocalRequest(dataset, rootEntry.node,
        LG_CLIPBOARD_FILE_LIST, 0, 0));
  KVMFRClipboardFileEntry stableEntry;
  KVMFRClipboardFileEntry changedEntry;
  CHECK(responseEntry("stable.txt", &stableEntry));
  CHECK(responseEntry("changed.txt", &changedEntry));

  char moved[320];
  CHECK(snprintf(moved, sizeof(moved), "%s-moved", directory) > 0);
  CHECK(rename(directory, moved) == 0);
  CHECK(mkdir(directory, 0700) == 0);
  char replacement[384];
  CHECK(snprintf(replacement, sizeof(replacement),
        "%s/stable.txt", directory) > 0);
  createFile(replacement, "replacement");

  CHECK(runLocalRequest(dataset, stableEntry.node,
        LG_CLIPBOARD_FILE_READ, 0, (uint32_t)stableEntry.size));
  CHECK(responseSize == strlen("stable"));
  CHECK(!memcmp(responseData, "stable", responseSize));
  clipboardFiles_testForceLocalEof();
  CHECK(!runLocalRequest(dataset, stableEntry.node,
        LG_CLIPBOARD_FILE_READ, 0, (uint32_t)stableEntry.size));
  CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_IO);

  char movedChanged[384];
  CHECK(snprintf(movedChanged, sizeof(movedChanged),
        "%s/changed.txt", moved) > 0);
  const int changedFd = open(movedChanged,
      O_WRONLY | O_APPEND | O_CLOEXEC);
  CHECK(changedFd >= 0);
  CHECK(write(changedFd, "-modified", 9) == 9);
  CHECK(close(changedFd) == 0);
  CHECK(!runLocalRequest(dataset, changedEntry.node,
        LG_CLIPBOARD_FILE_READ, 0, 64));
  CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_INVALID);
  CHECK(!runLocalRequest(dataset, changedEntry.node,
        LG_CLIPBOARD_FILE_READ, 0, (uint32_t)changedEntry.size));
  CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_STALE);

  char movedStable[384];
  char oldStable[384];
  CHECK(snprintf(movedStable, sizeof(movedStable),
        "%s/stable.txt", moved) > 0);
  CHECK(snprintf(oldStable, sizeof(oldStable),
        "%s/stable.old", moved) > 0);
  CHECK(rename(movedStable, oldStable) == 0);
  createFile(movedStable, "stable");
  CHECK(!runLocalRequest(dataset, stableEntry.node,
        LG_CLIPBOARD_FILE_READ, 0, 64));
  CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_INVALID);
  CHECK(!runLocalRequest(dataset, stableEntry.node,
        LG_CLIPBOARD_FILE_READ, 0, (uint32_t)stableEntry.size));
  CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_STALE);

  clipboardFiles_free();
  CHECK(unlink(replacement) == 0);
  CHECK(rmdir(directory) == 0);
  CHECK(unlink(movedStable) == 0);
  CHECK(unlink(oldStable) == 0);
  CHECK(unlink(movedChanged) == 0);
  CHECK(rmdir(moved) == 0);
}

static void testLocalUnsupportedEntry(void)
{
  char directory[] = "/tmp/lg-clipboard-unsupported-XXXXXX";
  CHECK(mkdtemp(directory));
  char target[256];
  char link[256];
  char fifo[256];
  CHECK(snprintf(target, sizeof(target),
        "%s/target.txt", directory) > 0);
  CHECK(snprintf(link, sizeof(link), "%s/link.txt", directory) > 0);
  CHECK(snprintf(fifo, sizeof(fifo), "%s/special", directory) > 0);
  createFile(target, "target");
  CHECK(symlink("target.txt", link) == 0);
  CHECK(mkfifo(fifo, 0600) == 0);

  CHECK(clipboardFiles_testInit(UINT64_C(0x62d1147362d11473)));
  char uri[320];
  const int uriLength = snprintf(
      uri, sizeof(uri), "file://%s\r\n", directory);
  CHECK(uriLength > 0 && (size_t)uriLength < sizeof(uri));
  CHECK(clipboardFiles_setLocal(
        "text/uri-list", uri, (size_t)uriLength));
  const uint64_t dataset = datasets[notices - 1U];
  CHECK(runLocalRequest(dataset, KVMFR_CLIPBOARD_FILE_ROOT_NODE,
        LG_CLIPBOARD_FILE_LIST, 0, 0));
  const char * base = strrchr(directory, '/');
  CHECK(base && base[1]);
  KVMFRClipboardFileEntry rootEntry;
  CHECK(responseEntry(base + 1, &rootEntry));
  CHECK(!runLocalRequest(dataset, rootEntry.node,
        LG_CLIPBOARD_FILE_LIST, 0, 0));
  CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED);

  clipboardFiles_free();
  CHECK(unlink(fifo) == 0);
  CHECK(unlink(link) == 0);
  CHECK(unlink(target) == 0);
  CHECK(rmdir(directory) == 0);
}

static uint64_t completeRemoteOffer(uint64_t dataset, const char * name)
{
  const unsigned previousAcquires = acquireCount;
  const unsigned previousRequests = requestCount;
  const unsigned previousReady = remoteReadyCount;
  CHECK(clipboardFiles_remoteOffer(dataset));
  CHECK(acquireCount == previousAcquires + 1U);
  CHECK(acquiredDataset == dataset);
  const uint64_t acquisition = acquiredRequest;

  clipboardFiles_remoteAcquired(dataset, acquisition,
      LG_CLIPBOARD_FILE_ERROR_NONE);
  CHECK(requestCount == previousRequests + 1U);
  CHECK(remoteRequest.dataset == dataset);
  CHECK(remoteRequest.node == KVMFR_CLIPBOARD_FILE_ROOT_NODE);
  CHECK(remoteRequest.operation == LG_CLIPBOARD_FILE_LIST);

  const size_t nameLength = strlen(name);
  const uint64_t wireSize = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(nameLength);
  CHECK(wireSize <= SIZE_MAX);
  uint8_t * wire = calloc(1, (size_t)wireSize);
  CHECK(wire);
  const KVMFRClipboardFileEntry entry =
  {
    .node       = 2,
    .size       = 9,
    .createdNs  = 1,
    .modifiedNs = 2,
    .type       = KVMFR_CLIPBOARD_FILE_TYPE_REGULAR,
    .nameLength = (uint16_t)nameLength,
  };
  memcpy(wire, &entry, sizeof(entry));
  memcpy(wire + sizeof(entry), name, nameLength);
  CHECK(clipboardFiles_remoteDataBegin(
        &remoteRequest, wireSize) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(clipboardFiles_remoteDataChunk(&remoteRequest, 0,
        wire, (size_t)wireSize) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(clipboardFiles_remoteDataEnd(
        &remoteRequest, wireSize) == LG_CLIPBOARD_RESULT_ACCEPTED);
  free(wire);
  CHECK(remoteReadyCount == previousReady + 1U);
  CHECK(clipboardFiles_remoteReady(dataset));
  return acquisition;
}

static uint64_t beginRemoteOffer(uint64_t dataset)
{
  const unsigned previousAcquires = acquireCount;
  CHECK(clipboardFiles_remoteOffer(dataset));
  CHECK(acquireCount == previousAcquires + 1U);
  CHECK(acquiredDataset == dataset);
  return acquiredRequest;
}

static LG_ClipboardFileRequest beginRemoteList(uint64_t dataset,
    uint64_t * acquisition)
{
  const unsigned previousRequests = requestCount;
  *acquisition = beginRemoteOffer(dataset);
  clipboardFiles_remoteAcquired(dataset, *acquisition,
      LG_CLIPBOARD_FILE_ERROR_NONE);
  CHECK(requestCount == previousRequests + 1U);
  CHECK(remoteRequest.dataset == dataset);
  CHECK(remoteRequest.operation == LG_CLIPBOARD_FILE_LIST);
  return remoteRequest;
}

static void checkRemoteFailure(uint64_t dataset,
    unsigned previousFailures, unsigned previousReleases,
    bool acquired)
{
  CHECK(remoteFailedCount == previousFailures + 1U);
  CHECK(remoteFailedDataset == dataset);
  CHECK(releaseCount == previousReleases + (acquired ? 1U : 0U));
  if (acquired)
    CHECK(releasedDataset == dataset);
  CHECK(clipboardFiles_testRemoteDatasetCount() == 0U);
}

static void testRemoteFailures(void)
{
  CHECK(clipboardFiles_testInit(UINT64_C(0x7315000073150000)));
  acquireCount = 0;
  releaseCount = 0;
  requestCount = 0;
  remoteFailedCount = 0;
  remoteFailedDataset = 0;
  remoteRequestSucceeds = true;

  uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x73150001);
  uint64_t acquisition = beginRemoteOffer(dataset);
  unsigned previousFailures = remoteFailedCount;
  unsigned previousReleases = releaseCount;
  clipboardFiles_remoteAcquired(dataset, acquisition,
      LG_CLIPBOARD_FILE_ERROR_ACCESS);
  checkRemoteFailure(dataset, previousFailures, previousReleases, false);

  dataset += 1U;
  acquisition = beginRemoteOffer(dataset);
  previousFailures = remoteFailedCount;
  previousReleases = releaseCount;
  clipboardFiles_remoteCancel(dataset, acquisition,
      LG_CLIPBOARD_FILE_ERROR_CANCELLED);
  checkRemoteFailure(dataset, previousFailures, previousReleases, false);

  dataset += 1U;
  LG_ClipboardFileRequest request = beginRemoteList(dataset, &acquisition);
  previousFailures = remoteFailedCount;
  previousReleases = releaseCount;
  clipboardFiles_remoteCancel(dataset, request.request,
      LG_CLIPBOARD_FILE_ERROR_CANCELLED);
  checkRemoteFailure(dataset, previousFailures, previousReleases, true);

  dataset += 1U;
  acquisition = beginRemoteOffer(dataset);
  previousFailures = remoteFailedCount;
  previousReleases = releaseCount;
  remoteRequestSucceeds = false;
  clipboardFiles_remoteAcquired(dataset, acquisition,
      LG_CLIPBOARD_FILE_ERROR_NONE);
  remoteRequestSucceeds = true;
  checkRemoteFailure(dataset, previousFailures, previousReleases, true);

  dataset += 1U;
  request = beginRemoteList(dataset, &acquisition);
  LG_ClipboardFileRequest malformed = request;
  malformed.node += 1U;
  previousFailures = remoteFailedCount;
  previousReleases = releaseCount;
  CHECK(clipboardFiles_remoteDataBegin(&malformed, 0) ==
      LG_CLIPBOARD_RESULT_FAILED);
  checkRemoteFailure(dataset, previousFailures, previousReleases, true);

  dataset += 1U;
  request = beginRemoteList(dataset, &acquisition);
  CHECK(clipboardFiles_remoteDataBegin(&request, 1) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  previousFailures = remoteFailedCount;
  previousReleases = releaseCount;
  CHECK(clipboardFiles_remoteDataChunk(&request, 0, NULL, 1) ==
      LG_CLIPBOARD_RESULT_FAILED);
  checkRemoteFailure(dataset, previousFailures, previousReleases, true);

  dataset += 1U;
  request = beginRemoteList(dataset, &acquisition);
  CHECK(clipboardFiles_remoteDataBegin(&request, 1) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  previousFailures = remoteFailedCount;
  previousReleases = releaseCount;
  CHECK(clipboardFiles_remoteDataEnd(&request, 0) ==
      LG_CLIPBOARD_RESULT_FAILED);
  checkRemoteFailure(dataset, previousFailures, previousReleases, true);

  dataset += 1U;
  request = beginRemoteList(dataset, &acquisition);
  static const char invalidName[] = "bad/name";
  const size_t wireSize = (size_t)KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(
      sizeof(invalidName) - 1U);
  uint8_t * wire = calloc(1, wireSize);
  CHECK(wire);
  const KVMFRClipboardFileEntry entry =
  {
    .node       = 2,
    .size       = 1,
    .type       = KVMFR_CLIPBOARD_FILE_TYPE_REGULAR,
    .nameLength = sizeof(invalidName) - 1U,
  };
  memcpy(wire, &entry, sizeof(entry));
  memcpy(wire + sizeof(entry), invalidName, sizeof(invalidName) - 1U);
  CHECK(clipboardFiles_remoteDataBegin(&request, wireSize) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(clipboardFiles_remoteDataChunk(&request, 0, wire, wireSize) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  previousFailures = remoteFailedCount;
  previousReleases = releaseCount;
  CHECK(clipboardFiles_remoteDataEnd(&request, wireSize) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  free(wire);
  checkRemoteFailure(dataset, previousFailures, previousReleases, true);

  clipboardFiles_free();
}

static void checkRemoteUri(uint64_t presentation, const char * name)
{
  char * uri = NULL;
  size_t size = 0;
  CHECK(clipboardFiles_getRemotePresentation(
        presentation, "text/uri-list", &uri, &size));
  CHECK(uri);
  CHECK(size == strlen(uri));
  CHECK(strstr(uri, name));
  free(uri);
}

static void checkRemoteMate(uint64_t presentation, const char * name)
{
  char * gnome = NULL;
  char * mate = NULL;
  size_t gnomeSize = 0;
  size_t mateSize = 0;
  CHECK(clipboardFiles_getRemotePresentation(presentation,
        "x-special/gnome-copied-files", &gnome, &gnomeSize));
  CHECK(clipboardFiles_getRemotePresentation(presentation,
        "x-special/mate-copied-files", &mate, &mateSize));
  CHECK(gnome);
  CHECK(mate);
  CHECK(gnomeSize == mateSize);
  CHECK(memcmp(gnome, mate, mateSize) == 0);
  CHECK(mateSize >= sizeof("copy\n") - 1U);
  CHECK(memcmp(mate, "copy\n", sizeof("copy\n") - 1U) == 0);
  CHECK(strstr(mate, name));
  free(mate);
  free(gnome);
}

typedef struct ReleasePresentationTask
{
  pthread_barrier_t * barrier;
  uint64_t            presentation;
}
ReleasePresentationTask;

static void * releasePresentation(void * opaque)
{
  ReleasePresentationTask * task = opaque;
  const int result = pthread_barrier_wait(task->barrier);
  CHECK(result == 0 || result == PTHREAD_BARRIER_SERIAL_THREAD);
  clipboardFiles_remotePresentationRelease(task->presentation);
  return NULL;
}

static void releasePresentationConcurrently(uint64_t presentation)
{
  pthread_barrier_t barrier;
  CHECK(pthread_barrier_init(&barrier, NULL, 3) == 0);
  ReleasePresentationTask task =
  {
    .barrier      = &barrier,
    .presentation = presentation,
  };
  pthread_t first;
  pthread_t second;
  CHECK(pthread_create(&first, NULL, releasePresentation, &task) == 0);
  CHECK(pthread_create(&second, NULL, releasePresentation, &task) == 0);
  const int result = pthread_barrier_wait(&barrier);
  CHECK(result == 0 || result == PTHREAD_BARRIER_SERIAL_THREAD);
  CHECK(pthread_join(first, NULL) == 0);
  CHECK(pthread_join(second, NULL) == 0);
  CHECK(pthread_barrier_destroy(&barrier) == 0);
}

static void testRemoteLifecycle(void)
{
  CHECK(clipboardFiles_testFuseStopWake());
  CHECK(clipboardFiles_testInit(UINT64_C(0x7311000073110000)));
  CHECK(clipboardFiles_testUnsentRemoteOwnership());
  acquireCount = 0;
  releaseCount = 0;
  requestCount = 0;
  remoteReadyCount = 0;
  cancelCount = 0;

  const uint64_t firstDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x73110001);
  const uint64_t first = completeRemoteOffer(firstDataset, "first.txt");

  CHECK(clipboardFiles_testRemoteRead(first, 2, 0, 9));
  LG_ClipboardFileRequest shortRead = remoteRequest;
  CHECK(clipboardFiles_remoteDataBegin(&shortRead, 8) ==
      LG_CLIPBOARD_RESULT_FAILED);

  CHECK(clipboardFiles_testRemoteRead(first, 2, 0, 9));
  shortRead = remoteRequest;
  CHECK(clipboardFiles_remoteDataBegin(&shortRead, 9) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  const uint8_t shortData[8] = { 0 };
  CHECK(clipboardFiles_remoteDataChunk(&shortRead, 0,
        shortData, sizeof(shortData)) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(clipboardFiles_remoteDataEnd(&shortRead, sizeof(shortData)) ==
      LG_CLIPBOARD_RESULT_FAILED);

  const uint64_t firstPresentation =
    clipboardFiles_remotePresentationAcquire();
  CHECK(firstPresentation == first);
  checkRemoteUri(firstPresentation, "first.txt");
  checkRemoteMate(firstPresentation, "first.txt");

  const uint64_t secondDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x73110002);
  completeRemoteOffer(secondDataset, "second.txt");
  CHECK(clipboardFiles_testRemoteDatasetCount() == 2U);
  CHECK(releaseCount == 0U);
  checkRemoteUri(firstPresentation, "first.txt");
  clipboardFiles_remotePresentationRelease(firstPresentation);
  CHECK(clipboardFiles_testRemoteDatasetCount() == 1U);
  CHECK(releaseCount == 1U);
  CHECK(releasedDataset == firstDataset);
  CHECK(releasedAcquisition == first);

  const uint64_t secondPresentation =
    clipboardFiles_remotePresentationAcquire();
  CHECK(secondPresentation != 0);
  checkRemoteUri(secondPresentation, "second.txt");
  clipboardFiles_remotePresentationDelivered(secondPresentation);
  clipboardFiles_remotePresentationRelease(secondPresentation);
  const uint64_t thirdDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x73110003);
  completeRemoteOffer(thirdDataset, "third.txt");
  CHECK(clipboardFiles_testRemoteDatasetCount() == 2U);
  CHECK(releaseCount == 1U);
  CHECK(clipboardFiles_testBeginRemoteLookup(secondPresentation));
  clipboardFiles_testExpireRemoteDeliveries();
  CHECK(clipboardFiles_testRemoteDatasetCount() == 2U);
  CHECK(releaseCount == 1U);
  clipboardFiles_testEndRemoteLookup(secondPresentation);
  CHECK(clipboardFiles_testRemoteDatasetCount() == 1U);
  CHECK(releaseCount == 2U);
  CHECK(releasedDataset == secondDataset);
  clipboardFiles_remoteClear();
  CHECK(clipboardFiles_testRemoteDatasetCount() == 0U);
  CHECK(releaseCount == 3U);

  for (uint64_t i = 0; i < 10; ++i)
  {
    const uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
      (UINT64_C(0x73120000) + i);
    const uint64_t acquisition =
      completeRemoteOffer(dataset, "grace.txt");
    const uint64_t presentation =
      clipboardFiles_remotePresentationAcquire();
    CHECK(presentation == acquisition);
    checkRemoteUri(presentation, "grace.txt");
    clipboardFiles_remotePresentationDelivered(presentation);
    clipboardFiles_remotePresentationRelease(presentation);
    CHECK(clipboardFiles_testRemoteDatasetCount() <= 5U);
  }
  clipboardFiles_remoteClear();
  clipboardFiles_testExpireRemoteDeliveries();
  CHECK(clipboardFiles_testRemoteDatasetCount() == 0U);

  for (uint64_t i = 0; i < 16; ++i)
  {
    const uint64_t oldDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
      (UINT64_C(0x73130000) + i * 2U);
    const uint64_t oldAcquisition =
      completeRemoteOffer(oldDataset, "old.txt");
    const uint64_t presentation =
      clipboardFiles_remotePresentationAcquire();
    CHECK(presentation == oldAcquisition);
    const uint64_t currentDataset = oldDataset + 1U;
    completeRemoteOffer(currentDataset, "current.txt");
    const unsigned previousReleases = releaseCount;
    releasePresentationConcurrently(presentation);
    CHECK(releaseCount == previousReleases + 1U);
    CHECK(releasedDataset == oldDataset);
    CHECK(clipboardFiles_testRemoteDatasetCount() == 1U);
    clipboardFiles_remoteClear();
    CHECK(clipboardFiles_testRemoteDatasetCount() == 0U);
  }

  const uint64_t pendingDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x73140001);
  CHECK(clipboardFiles_remoteOffer(pendingDataset));
  CHECK(acquiredDataset == pendingDataset);
  const uint64_t pendingAcquisition = acquiredRequest;
  const uint64_t replacementDataset = pendingDataset + 1U;
  const unsigned previousCancels = cancelCount;
  CHECK(clipboardFiles_remoteOffer(replacementDataset));
  CHECK(cancelCount == previousCancels + 1U);
  CHECK(cancelledDataset == pendingDataset);
  CHECK(cancelledRequest == pendingAcquisition);
  CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_CANCELLED);
  CHECK(clipboardFiles_testRemoteDatasetCount() == 1U);
  clipboardFiles_remoteClear();
  CHECK(clipboardFiles_testRemoteDatasetCount() == 0U);
  clipboardFiles_free();
}

static int testMount(void)
{
  if (access("/dev/fuse", R_OK | W_OK) < 0)
    return 77;
  if (!getenv("XDG_RUNTIME_DIR"))
    CHECK(setenv("XDG_RUNTIME_DIR", "/tmp", 1) == 0);
  CHECK(clipboardFiles_init());
  clipboardFiles_free();
  return 0;
}

int main(int argc, char * argv[])
{
  debug_init();

  if (argc == 2 && !strcmp(argv[1], "mount"))
    return testMount();
  if (argc == 2 && !strcmp(argv[1], "failures"))
  {
    testRemoteFailures();
    return 0;
  }
  testLocalLifecycle();
  testLocalUriValidation();
  testLocalSnapshot();
  testLocalUnsupportedEntry();
  testRemoteLifecycle();
  free(responseData);
  return 0;
}
