/**
 * 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 "clipboard.h"

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

#include <lgmp/client.h>
#include <lgmp/host.h>
#include <lgmp/stream.h>

#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

#define TEST_WAIT_MS          2500U
#define TEST_QUIET_MS         50U
#define TEST_MEMORY_MAX       128U
#define SLOT_BYTES            KVMFR_CLIPBOARD_STREAM_RECORD_BYTES
#define TEST_SHM_OVERHEAD     (1024U * 1024U)
/* Match the production duplex streams and leave room for LGMP metadata. */
#define TEST_SHM_SIZE         \
  (2U * KVMFR_CLIPBOARD_STREAM_SLOT_COUNT * SLOT_BYTES + \
    TEST_SHM_OVERHEAD)

#define CHECK(x) \
  do \
  { \
    if (!(x)) \
    { \
      fprintf(stderr, "check failed at %s:%d: %s\n", \
          __FILE__, __LINE__, #x); \
      return false; \
    } \
  } \
  while (0)

typedef struct EventTrace
{
  atomic_uint notice;
  atomic_uint release;
  atomic_uint request;
  atomic_uint requestCancel;
  atomic_uint dataBegin;
  atomic_uint dataChunk;
  atomic_uint dataEnd;
  atomic_uint dataCancel;
  atomic_uint dataReady;
  atomic_uint fileOffer;
  atomic_uint fileAcquire;
  atomic_uint fileAcquired;
  atomic_uint fileRequest;
  atomic_uint fileCancel;
  atomic_bool acceptRequest;
  atomic_bool blockChunkOnce;
  atomic_bool attached;
  atomic_bool fileOfferBeforeNotice;
  LG_ClipboardData noticeTypes[LG_CLIPBOARD_DATA_NONE];
  size_t noticeCount;
  LG_ClipboardRequest requestID;
  LG_ClipboardData requestType;
  LG_ClipboardRequest streamID;
  LG_ClipboardData streamType;
  uint64_t sizeHint;
  uint64_t chunkOffset;
  size_t chunkSize;
  uint8_t chunkData[64];
  uint64_t finalSize;
  LG_ClipboardRequest cancelID;
  LG_ClipboardCancelReason cancelReason;
  uint64_t fileDataset;
  uint64_t fileAcquisition;
  LG_ClipboardFileError fileError;
  LG_ClipboardFileRequest fileRequestValue;
}
EventTrace;

typedef struct TestState
{
  void                  * memory;
  PLGMPHost               host;
  PLGMPHostQueue          queue;
  PLGMPClient             client;
  PLGMPHostStream         hostToClientStream;
  PLGMPHostStream         clientToHostStream;
  KVMFRStreamDescriptor   hostToClientDescriptor;
  KVMFRStreamDescriptor   clientToHostDescriptor;
  bool                    streamsBound;
  LGMPClipboard         * clipboard;
  const LG_ClipboardOps * ops;
  uint32_t                clientID;
  uint32_t                hostSerial;
  uint64_t                localClipboardGeneration;
  PLGMPMemory             memories[TEST_MEMORY_MAX];
  unsigned                memoryCount;
  atomic_uint             statusCount;
  atomic_bool             statusAvailable;
  atomic_uint             statusGeneration;
  atomic_bool             autoAttach;
  EventTrace              events;
}
TestState;

typedef struct DisconnectTask
{
  LGMPClipboard * clipboard;
  atomic_bool      done;
}
DisconnectTask;

static bool disconnectAndDrain(TestState * state,
    uint32_t * releaseGeneration);

static void notice(void * opaque, const LG_ClipboardData types[],
    size_t count)
{
  EventTrace * trace = opaque;
  if (count == 1 && types[0] == LG_CLIPBOARD_DATA_FILES)
    atomic_store_explicit(&trace->fileOfferBeforeNotice,
        atomic_load_explicit(&trace->fileOffer, memory_order_acquire) != 0,
        memory_order_release);
  trace->noticeCount = count;
  if (count)
    memcpy(trace->noticeTypes, types, count * sizeof(*types));
  atomic_fetch_add_explicit(&trace->notice, 1, memory_order_release);
}

static void releaseNotice(void * opaque)
{
  EventTrace * trace = opaque;
  atomic_fetch_add_explicit(&trace->release, 1, memory_order_release);
}

static bool request(void * opaque, LG_ClipboardRequest id,
    LG_ClipboardData type)
{
  EventTrace * trace = opaque;
  trace->requestID   = id;
  trace->requestType = type;
  atomic_fetch_add_explicit(&trace->request, 1, memory_order_release);
  return atomic_load_explicit(&trace->acceptRequest, memory_order_acquire);
}

static LG_ClipboardResult dataBegin(void * opaque,
    LG_ClipboardRequest id, LG_ClipboardData type, uint64_t sizeHint)
{
  EventTrace * trace = opaque;
  trace->streamID   = id;
  trace->streamType = type;
  trace->sizeHint   = sizeHint;
  atomic_fetch_add_explicit(&trace->dataBegin, 1, memory_order_release);
  return LG_CLIPBOARD_RESULT_ACCEPTED;
}

static LG_ClipboardResult dataChunk(void * opaque,
    LG_ClipboardRequest id, uint64_t offset,
    const void * data, size_t size)
{
  EventTrace * trace = opaque;
  trace->streamID    = id;
  trace->chunkOffset = offset;
  trace->chunkSize   = size;
  if (size <= sizeof(trace->chunkData))
    memcpy(trace->chunkData, data, size);
  atomic_fetch_add_explicit(&trace->dataChunk, 1, memory_order_release);
  if (atomic_exchange_explicit(&trace->blockChunkOnce, false,
      memory_order_acq_rel))
    return LG_CLIPBOARD_RESULT_BLOCKED;
  return LG_CLIPBOARD_RESULT_ACCEPTED;
}

static LG_ClipboardResult dataEnd(void * opaque,
    LG_ClipboardRequest id, uint64_t size)
{
  EventTrace * trace = opaque;
  trace->streamID  = id;
  trace->finalSize = size;
  atomic_fetch_add_explicit(&trace->dataEnd, 1, memory_order_release);
  return LG_CLIPBOARD_RESULT_ACCEPTED;
}

static void dataCancelEvent(void * opaque, LG_ClipboardRequest id,
    LG_ClipboardCancelReason reason)
{
  EventTrace * trace = opaque;
  trace->cancelID     = id;
  trace->cancelReason = reason;
  atomic_fetch_add_explicit(&trace->dataCancel, 1, memory_order_release);
}

static void dataReadyEvent(void * opaque, LG_ClipboardRequest id)
{
  EventTrace * trace = opaque;
  trace->streamID = id;
  atomic_fetch_add_explicit(&trace->dataReady, 1, memory_order_release);
}

static void requestCancelEvent(void * opaque, LG_ClipboardRequest id,
    LG_ClipboardCancelReason reason)
{
  EventTrace * trace = opaque;
  trace->cancelID     = id;
  trace->cancelReason = reason;
  atomic_fetch_add_explicit(
      &trace->requestCancel, 1, memory_order_release);
}

static void fileOfferEvent(void * opaque, uint64_t dataset)
{
  EventTrace * trace = opaque;
  trace->fileDataset = dataset;
  atomic_fetch_add_explicit(&trace->fileOffer, 1, memory_order_release);
}

static void fileAcquireEvent(void * opaque, uint64_t dataset,
    uint64_t acquisition)
{
  EventTrace * trace = opaque;
  trace->fileDataset     = dataset;
  trace->fileAcquisition = acquisition;
  atomic_fetch_add_explicit(&trace->fileAcquire, 1, memory_order_release);
}

static void fileAcquiredEvent(void * opaque, uint64_t dataset,
    uint64_t acquisition, LG_ClipboardFileError error)
{
  EventTrace * trace = opaque;
  trace->fileDataset     = dataset;
  trace->fileAcquisition = acquisition;
  trace->fileError       = error;
  atomic_fetch_add_explicit(&trace->fileAcquired, 1, memory_order_release);
}

static void fileRequestEvent(void * opaque,
    const LG_ClipboardFileRequest * request)
{
  EventTrace * trace = opaque;
  trace->fileRequestValue = *request;
  atomic_fetch_add_explicit(&trace->fileRequest, 1, memory_order_release);
}

static void fileCancelEvent(void * opaque, uint64_t dataset,
    uint64_t request, LG_ClipboardFileError error)
{
  EventTrace * trace = opaque;
  trace->fileDataset     = dataset;
  trace->fileAcquisition = request;
  trace->fileError       = error;
  atomic_fetch_add_explicit(&trace->fileCancel, 1, memory_order_release);
}

static const LG_ClipboardEventOps EVENT_OPS =
{
  .notice        = notice,
  .dataBegin     = dataBegin,
  .dataChunk     = dataChunk,
  .dataEnd       = dataEnd,
  .dataCancel    = dataCancelEvent,
  .dataReady     = dataReadyEvent,
  .requestCancel = requestCancelEvent,
  .release       = releaseNotice,
  .request       = request,
  .fileOffer     = fileOfferEvent,
  .fileAcquire   = fileAcquireEvent,
  .fileAcquired  = fileAcquiredEvent,
  .fileRequest   = fileRequestEvent,
  .fileCancel    = fileCancelEvent,
};

static void statusChanged(void * opaque, const LG_ClipboardStatus * status)
{
  TestState * state = opaque;
  atomic_store_explicit(&state->statusAvailable,
    status->available, memory_order_release);
  atomic_store_explicit(&state->statusGeneration,
    status->generation, memory_order_release);
  atomic_fetch_add_explicit(&state->statusCount, 1, memory_order_release);

  if (status->available &&
      atomic_load_explicit(&state->autoAttach, memory_order_acquire) &&
      !atomic_exchange_explicit(
        &state->events.attached, true, memory_order_acq_rel))
  {
    const bool attached = state->ops->attach(
      state->clipboard, &EVENT_OPS, &state->events);
    atomic_store_explicit(
      &state->events.attached, attached, memory_order_release);
  }
}

static bool hostProcess(TestState * state)
{
  CHECK(lgmpHostProcess(state->host) == LGMP_OK);
  return true;
}

static KVMFRStreamDescriptor exportStreamDescriptor(
    const struct LGMPStreamDescriptor * descriptor)
{
  return (KVMFRStreamDescriptor)
  {
    .magic      = descriptor->magic,
    .version    = descriptor->version,
    .size       = descriptor->size,
    .offset     = descriptor->offset,
    .regionSize = descriptor->regionSize,
    .direction  = descriptor->direction,
    .policy     = descriptor->policy,
    .slotCount  = descriptor->slotCount,
    .slotSize   = descriptor->slotSize,
  };
}

static bool waitAtomic(TestState * state, atomic_uint * value,
    unsigned expected)
{
  for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
  {
    CHECK(hostProcess(state));
    if (atomic_load_explicit(value, memory_order_acquire) >= expected)
      return true;
    usleep(1000);
  }
  return false;
}

static bool waitMemory(TestState * state, PLGMPMemory memory)
{
  for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
  {
    CHECK(hostProcess(state));
    if (!lgmpHostQueuePayloadPending(state->queue, memory))
      return true;
    usleep(1000);
  }
  return false;
}

static bool allocMemory(TestState * state, size_t size,
    PLGMPMemory * result)
{
  CHECK(state->memoryCount < TEST_MEMORY_MAX);
  CHECK(lgmpHostMemAlloc(state->host, (uint32_t)size, result) == LGMP_OK);
  state->memories[state->memoryCount++] = *result;
  memset(lgmpHostMemPtr(*result), 0, size);
  return true;
}

static bool postMemory(TestState * state, PLGMPMemory memory,
    KVMFRClipboardQueueType type, uint32_t serial)
{
  const uint64_t udata = KVMFR_CLIPBOARD_QUEUE_UDATA(type, serial);
  for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
  {
    unsigned recipients = 0;
    const LGMP_STATUS status = lgmpHostQueuePostForClients(state->queue,
      udata, memory, &state->clientID, 1, &recipients);
    if (status == LGMP_OK)
      return recipients == 1;
    CHECK(status == LGMP_ERR_QUEUE_FULL);
    CHECK(hostProcess(state));
    usleep(1000);
  }
  return false;
}

static uint32_t nextSerial(TestState * state)
{
  if (++state->hostSerial == 0)
    ++state->hostSerial;
  return state->hostSerial;
}

static bool unbindStream(TestState * state, PLGMPHostStream stream)
{
  for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
  {
    const LGMP_STATUS status = lgmpHostStreamUnbind(stream);
    if (status == LGMP_OK)
      return true;
    CHECK(status == LGMP_ERR_STREAM_BUSY);
    CHECK(hostProcess(state));
    usleep(1000);
  }
  return false;
}

static bool postStatus(TestState * state, uint32_t endpointGeneration,
    uint32_t ownerClientID, uint32_t ownerGeneration,
    bool available, KVMFRClipboardFormatFlags formats)
{
  if (!ownerClientID && state->streamsBound)
  {
    CHECK(unbindStream(state, state->clientToHostStream));
    CHECK(unbindStream(state, state->hostToClientStream));
    state->streamsBound = false;
  }

  PLGMPMemory memory;
  CHECK(allocMemory(state, sizeof(KVMFRClipboardStatus), &memory));
  KVMFRClipboardStatus * status = lgmpHostMemPtr(memory);
  *status = (KVMFRClipboardStatus)
  {
    .version         = KVMFR_CLIPBOARD_VERSION,
    .flags           = (available ? KVMFR_CLIPBOARD_STATUS_AVAILABLE : 0) |
      (ownerClientID ? KVMFR_CLIPBOARD_STATUS_HAS_OWNER : 0),
    .generation      = endpointGeneration,
    .ownerClientID   = ownerClientID,
    .ownerGeneration = ownerGeneration,
    .lease            = 1000,
    .formats          = available ? formats : 0,
    .slotBytes        = KVMFR_CLIPBOARD_STREAM_SLOT_BYTES,
    .streamVersion    = KVMFR_CLIPBOARD_STREAM_VERSION,
    .streamSlotCount  = KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
    .hostToClient     = state->hostToClientDescriptor,
    .clientToHost     = state->clientToHostDescriptor,
  };
  CHECK(postMemory(state, memory, KVMFR_CLIPBOARD_QUEUE_STATUS,
        nextSerial(state)));
  CHECK(waitMemory(state, memory));
  return true;
}

static bool postRecord(TestState * state,
    const KVMFRClipboardMessage * record,
    KVMFRClipboardQueueType type, const void * data)
{
  CHECK(type == KVMFR_CLIPBOARD_QUEUE_MESSAGE);
  CHECK(record->length == 0);
  CHECK(data == NULL);
  PLGMPMemory memory;
  CHECK(allocMemory(state, sizeof(*record), &memory));
  memcpy(lgmpHostMemPtr(memory), record, sizeof(*record));
  CHECK(postMemory(state, memory, type, nextSerial(state)));
  return true;
}

static bool postStreamRecord(TestState * state,
    const KVMFRClipboardMessage * record, const void * data)
{
  CHECK(state->streamsBound);
  CHECK(record->length <= KVMFR_CLIPBOARD_STREAM_SLOT_BYTES);
  CHECK(!record->length || data);
  for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
  {
    CHECK(hostProcess(state));
    LGMPStreamBuffer buffer = { 0 };
    const LGMP_STATUS status = lgmpHostStreamWriteAcquire(
      state->hostToClientStream, &buffer);
    if (status == LGMP_ERR_STREAM_FULL)
    {
      usleep(1000);
      continue;
    }
    CHECK(status == LGMP_OK);
    const uint32_t bytes = sizeof(*record) + record->length;
    CHECK(buffer.capacity >= bytes);
    memcpy(buffer.data, record, sizeof(*record));
    if (record->length)
      memcpy((uint8_t *)buffer.data + sizeof(*record), data,
        record->length);
    CHECK(lgmpHostStreamWriteCommit(state->hostToClientStream,
          &buffer, bytes) == LGMP_OK);
    return true;
  }
  return false;
}

static bool settleHostStream(TestState * state)
{
  for (unsigned i = 0; i < TEST_QUIET_MS; ++i)
  {
    CHECK(hostProcess(state));
    usleep(1000);
  }
  return true;
}

static bool expectPayloadQueueClean(TestState * state)
{
  for (;;)
  {
    KVMFRClipboardMessage record;
    size_t size = sizeof(record);
    uint32_t clientID = 0;
    const LGMP_STATUS status = lgmpHostReadDataWithSource(
      state->queue, &record, &size, &clientID);
    if (status == LGMP_ERR_QUEUE_EMPTY)
      return true;
    CHECK(status == LGMP_OK);
    CHECK(size == sizeof(record));
    CHECK(clientID == state->clientID);
    CHECK(lgmpHostAckData(state->queue) == LGMP_OK);
    CHECK(record.type == KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE);
  }
}

static bool readClientStream(TestState * state,
    KVMFRClipboardMessage * record, LGMPStreamBuffer * buffer,
    const void ** data)
{
  for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
  {
    CHECK(hostProcess(state));
    CHECK(expectPayloadQueueClean(state));
    memset(buffer, 0, sizeof(*buffer));
    const LGMP_STATUS status = lgmpHostStreamReadPeek(
      state->clientToHostStream, buffer);
    if (status == LGMP_ERR_STREAM_EMPTY)
    {
      usleep(1000);
      continue;
    }
    CHECK(status == LGMP_OK);
    CHECK(buffer->size >= sizeof(*record));
    memcpy(record, buffer->data, sizeof(*record));
    CHECK(buffer->size == sizeof(*record) + record->length);
    *data = (const uint8_t *)buffer->data + sizeof(*record);
    return true;
  }
  return false;
}

static bool releaseClientStream(TestState * state,
    LGMPStreamBuffer * buffer)
{
  CHECK(lgmpHostStreamReadRelease(
        state->clientToHostStream, buffer) == LGMP_OK);
  memset(buffer, 0, sizeof(*buffer));
  return true;
}

static bool readClient(TestState * state, KVMFRClipboardMessage * record)
{
  for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
  {
    CHECK(hostProcess(state));
    size_t size = sizeof(*record);
    uint32_t clientID = 0;
    const LGMP_STATUS status = lgmpHostReadDataWithSource(
      state->queue, record, &size, &clientID);
    if (status == LGMP_ERR_QUEUE_EMPTY)
    {
      usleep(1000);
      continue;
    }
    CHECK(status == LGMP_OK);
    CHECK(size == sizeof(*record));
    CHECK(clientID == state->clientID);
    CHECK(record->type != KVMFR_CLIPBOARD_MESSAGE_DATA);
    CHECK(record->type != KVMFR_CLIPBOARD_MESSAGE_FILE_DATA);
    CHECK(lgmpHostAckData(state->queue) == LGMP_OK);
    return true;
  }
  return false;
}

static bool readType(TestState * state, KVMFRClipboardMessageType type,
    KVMFRClipboardMessage * record)
{
  for (unsigned i = 0; i < 32; ++i)
  {
    CHECK(readClient(state, record));
    if (record->type == type)
      return true;
  }
  return false;
}

static bool readNonKeepalive(TestState * state,
    KVMFRClipboardMessage * record)
{
  for (unsigned i = 0; i < 32; ++i)
  {
    CHECK(readClient(state, record));
    if (record->type != KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE)
      return true;
  }
  return false;
}

static bool noClientControl(TestState * state)
{
  for (unsigned i = 0; i < TEST_QUIET_MS; ++i)
  {
    CHECK(hostProcess(state));
    KVMFRClipboardMessage record;
    size_t size = sizeof(record);
    uint32_t clientID = 0;
    const LGMP_STATUS status = lgmpHostReadDataWithSource(
      state->queue, &record, &size, &clientID);
    if (status == LGMP_ERR_QUEUE_EMPTY)
    {
      usleep(1000);
      continue;
    }
    if (status == LGMP_OK)
      lgmpHostAckData(state->queue);
    CHECK(status == LGMP_ERR_QUEUE_EMPTY);
  }
  return true;
}

static bool claim(TestState * state, uint32_t endpointGeneration,
    bool autoAttach, KVMFRClipboardMessage * result)
{
  atomic_store(&state->autoAttach, autoAttach);
  CHECK(postStatus(state, endpointGeneration, 0, 0, true, 0));
  if (autoAttach)
  {
    for (unsigned i = 0; i < TEST_WAIT_MS &&
        !atomic_load(&state->events.attached); ++i)
    {
      CHECK(hostProcess(state));
      usleep(1000);
    }
    CHECK(atomic_load(&state->events.attached));
  }
  else
  {
    CHECK(state->ops->attach(
          state->clipboard, &EVENT_OPS, &state->events));
    atomic_store(&state->events.attached, true);
  }
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, result));
  CHECK(result->version == KVMFR_CLIPBOARD_VERSION);
  CHECK(result->generation != 0);
  CHECK(result->token == endpointGeneration);
  CHECK(result->flags == 0);
  return true;
}

static bool own(TestState * state, uint32_t endpointGeneration,
    uint32_t claimGeneration)
{
  if (!state->streamsBound)
  {
    CHECK(lgmpHostStreamBind(state->hostToClientStream,
          state->clientID, NULL) == LGMP_OK);
    if (lgmpHostStreamBind(state->clientToHostStream,
          state->clientID, NULL) != LGMP_OK)
    {
      lgmpHostStreamUnbind(state->hostToClientStream);
      return false;
    }
    state->streamsBound = true;
  }
  CHECK(postStatus(state, endpointGeneration, state->clientID,
        claimGeneration, true, 0));
  return true;
}

static KVMFRClipboardMessage hostRecord(
    KVMFRClipboardMessageType type, uint32_t generation)
{
  return (KVMFRClipboardMessage)
  {
    .version    = KVMFR_CLIPBOARD_VERSION,
    .type       = type,
    .generation = generation,
  };
}

static bool offerAndRequest(TestState * state, uint32_t generation,
    uint64_t clipboardGeneration, LG_ClipboardRequest appRequest,
    KVMFRClipboardMessage * wireRequest)
{
  KVMFRClipboardMessage offer =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, generation);
  offer.clipboardGeneration = clipboardGeneration;
  offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_TEXT |
    KVMFR_CLIPBOARD_FORMAT_MASK_PNG;
  const unsigned notices = atomic_load(&state->events.notice) + 1;
  CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.notice, notices));
  CHECK(state->events.noticeCount == 2);
  CHECK(state->events.noticeTypes[0] == LG_CLIPBOARD_DATA_TEXT);
  CHECK(state->events.noticeTypes[1] == LG_CLIPBOARD_DATA_PNG);
  CHECK(!state->ops->request(state->clipboard,
        appRequest + 1, LG_CLIPBOARD_DATA_JPEG));
  CHECK(state->ops->request(state->clipboard,
        appRequest, LG_CLIPBOARD_DATA_TEXT));
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_REQUEST, wireRequest));
  CHECK(kvmfrClipboardTransferFromClient(wireRequest->transfer));
  CHECK(wireRequest->generation == generation);
  CHECK(wireRequest->clipboardGeneration == clipboardGeneration);
  CHECK(wireRequest->format == KVMFR_CLIPBOARD_FORMAT_TEXT);
  return true;
}

static bool testClaim(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 10, true, &claimRecord));
  CHECK(own(state, 10, claimRecord.generation));

  const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
  CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
  KVMFRClipboardMessage offer;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer));
  state->localClipboardGeneration = offer.clipboardGeneration;
  CHECK(offer.generation == claimRecord.generation);
  CHECK(offer.clipboardGeneration != 0);
  CHECK(offer.token == KVMFR_CLIPBOARD_FORMAT_MASK_TEXT);
  return true;
}

static bool testOfferRequest(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 20, false, &claimRecord));
  CHECK(own(state, 20, claimRecord.generation));
  KVMFRClipboardMessage requestRecord;
  CHECK(offerAndRequest(state, claimRecord.generation,
        200, 77, &requestRecord));
  return true;
}

static bool testInboundStream(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 30, false, &claimRecord));
  CHECK(own(state, 30, claimRecord.generation));
  KVMFRClipboardMessage requestRecord;
  CHECK(offerAndRequest(state, claimRecord.generation,
        300, 88, &requestRecord));

  const uint8_t bytes[] = { 1, 2, 3, 4, 5 };
  KVMFRClipboardMessage data =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
  data.clipboardGeneration = 300;
  data.transfer = requestRecord.transfer;
  data.size     = 5;
  data.format   = KVMFR_CLIPBOARD_FORMAT_TEXT;
  data.flags    = KVMFR_CLIPBOARD_FLAG_BEGIN;
  CHECK(postStreamRecord(state, &data, NULL));
  CHECK(waitAtomic(state, &state->events.dataBegin, 1));
  CHECK(atomic_load(&state->events.dataBegin) == 1);
  CHECK(atomic_load(&state->events.dataChunk) == 0);
  CHECK(state->events.streamID == 88);
  CHECK(state->events.sizeHint == 5);

  data.sequence = 1;
  data.size     = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
  data.flags    = 0;
  data.length   = sizeof(bytes);
  CHECK(postStreamRecord(state, &data, bytes));
  CHECK(waitAtomic(state, &state->events.dataChunk, 1));
  CHECK(state->events.chunkOffset == 0);
  CHECK(state->events.chunkSize == sizeof(bytes));
  CHECK(memcmp(state->events.chunkData, bytes, sizeof(bytes)) == 0);

  data.sequence = 2;
  data.offset   = sizeof(bytes);
  data.size     = sizeof(bytes);
  data.flags    = KVMFR_CLIPBOARD_FLAG_END;
  data.length   = 0;
  CHECK(postStreamRecord(state, &data, NULL));
  CHECK(waitAtomic(state, &state->events.dataEnd, 1));
  CHECK(atomic_load(&state->events.dataChunk) == 1);
  CHECK(state->events.finalSize == 5);

  KVMFRClipboardMessage request2;
  CHECK(offerAndRequest(state, claimRecord.generation,
        301, 89, &request2));
  data = hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA,
    claimRecord.generation);
  data.clipboardGeneration = 301;
  data.transfer = request2.transfer;
  data.format   = KVMFR_CLIPBOARD_FORMAT_TEXT;
  data.flags    = KVMFR_CLIPBOARD_FLAG_BEGIN |
    KVMFR_CLIPBOARD_FLAG_END;
  CHECK(postStreamRecord(state, &data, NULL));
  CHECK(waitAtomic(state, &state->events.dataEnd, 2));
  CHECK(atomic_load(&state->events.dataBegin) == 2);
  CHECK(state->events.finalSize == 0);
  return true;
}

static bool prepareOutbound(TestState * state, uint32_t endpoint,
    KVMFRClipboardMessage * claimRecord, uint64_t transfer)
{
  CHECK(claim(state, endpoint, false, claimRecord));
  CHECK(own(state, endpoint, claimRecord->generation));
  const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
  CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
  KVMFRClipboardMessage offer;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer));
  state->localClipboardGeneration = offer.clipboardGeneration;

  KVMFRClipboardMessage requestRecord =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST,
      claimRecord->generation);
  requestRecord.clipboardGeneration = offer.clipboardGeneration;
  requestRecord.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | transfer;
  requestRecord.format   = KVMFR_CLIPBOARD_FORMAT_TEXT;
  const unsigned requests = atomic_load(&state->events.request) + 1;
  CHECK(postRecord(state, &requestRecord,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.request, requests));
  CHECK(state->events.requestID == requestRecord.transfer);
  CHECK(state->events.requestType == LG_CLIPBOARD_DATA_TEXT);
  return true;
}

static bool checkDataRecord(TestState * state,
    KVMFRClipboardMessage * record, LGMPStreamBuffer * buffer,
    const void ** data)
{
  CHECK(readClientStream(state, record, buffer, data));
  CHECK(record->type == KVMFR_CLIPBOARD_MESSAGE_DATA);
  CHECK(record->version == KVMFR_CLIPBOARD_VERSION);
  return true;
}

static bool checkFileDataRecord(TestState * state,
    KVMFRClipboardFileOperation operation,
    KVMFRClipboardMessage * record, LGMPStreamBuffer * buffer,
    const void ** data)
{
  CHECK(readClientStream(state, record, buffer, data));
  CHECK(record->type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA);
  CHECK(record->version == KVMFR_CLIPBOARD_VERSION);
  CHECK(record->token == operation);
  return true;
}

static bool testOutboundStream(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(prepareOutbound(state, 40, &claimRecord, 1));
  const LG_ClipboardRequest transfer = state->events.requestID;

  CHECK(state->ops->dataBegin(state->clipboard, transfer,
        LG_CLIPBOARD_DATA_TEXT, 5) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(noClientControl(state));
  const uint8_t first[] = { 1, 2, 3 };
  CHECK(state->ops->dataChunk(state->clipboard, transfer,
        0, first, sizeof(first)) == LG_CLIPBOARD_RESULT_ACCEPTED);
  KVMFRClipboardMessage record;
  LGMPStreamBuffer buffer;
  const void * data = NULL;
  CHECK(checkDataRecord(state, &record, &buffer, &data));
  CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_BEGIN);
  CHECK(record.sequence == 0 && record.offset == 0 && record.size == 5);
  CHECK(memcmp(data, first, sizeof(first)) == 0);
  CHECK(releaseClientStream(state, &buffer));

  const uint8_t second[] = { 4, 5 };
  CHECK(state->ops->dataChunk(state->clipboard, transfer,
        3, second, sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(checkDataRecord(state, &record, &buffer, &data));
  CHECK(record.flags == 0 && record.sequence == 1 && record.offset == 3);
  CHECK(record.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN);
  CHECK(memcmp(data, second, sizeof(second)) == 0);
  CHECK(releaseClientStream(state, &buffer));

  CHECK(state->ops->dataEnd(state->clipboard, transfer, 5) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(checkDataRecord(state, &record, &buffer, &data));
  CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_END);
  CHECK(record.sequence == 2 && record.offset == 5 && record.size == 5);
  CHECK(record.length == 0);
  CHECK(releaseClientStream(state, &buffer));

  KVMFRClipboardMessage requestRecord =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST,
      claimRecord.generation);
  requestRecord.clipboardGeneration = record.clipboardGeneration;
  requestRecord.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | 2;
  requestRecord.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
  CHECK(postRecord(state, &requestRecord,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.request, 2));
  CHECK(state->ops->dataBegin(state->clipboard, requestRecord.transfer,
        LG_CLIPBOARD_DATA_TEXT, 0) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(state->ops->dataEnd(state->clipboard,
        requestRecord.transfer, 0) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(checkDataRecord(state, &record, &buffer, &data));
  CHECK(record.flags == (KVMFR_CLIPBOARD_FLAG_BEGIN |
        KVMFR_CLIPBOARD_FLAG_END));
  CHECK(record.sequence == 0 && record.offset == 0 && record.size == 0);
  CHECK(releaseClientStream(state, &buffer));
  return true;
}

static bool testBlocked(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(prepareOutbound(state, 50, &claimRecord, 3));
  const LG_ClipboardRequest transfer = state->events.requestID;
  CHECK(state->ops->dataBegin(state->clipboard, transfer,
        LG_CLIPBOARD_DATA_TEXT, 5) == LG_CLIPBOARD_RESULT_ACCEPTED);
  const uint8_t bytes[] = { 8, 9, 10, 11, 12 };
  for (unsigned i = 0; i < KVMFR_CLIPBOARD_STREAM_SLOT_COUNT; ++i)
    CHECK(state->ops->dataChunk(state->clipboard, transfer,
          i, &bytes[i], 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(state->ops->dataChunk(state->clipboard, transfer,
        4, &bytes[4], 1) == LG_CLIPBOARD_RESULT_BLOCKED);

  KVMFRClipboardMessage record;
  LGMPStreamBuffer buffer;
  const void * data = NULL;
  CHECK(checkDataRecord(state, &record, &buffer, &data));
  CHECK(record.sequence == 0 && record.offset == 0 && record.length == 1);
  CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_BEGIN);
  CHECK(*(const uint8_t *)data == bytes[0]);
  CHECK(releaseClientStream(state, &buffer));
  CHECK(waitAtomic(state, &state->events.dataReady, 1));
  CHECK(state->events.streamID == transfer);
  CHECK(state->ops->dataChunk(state->clipboard, transfer,
        4, &bytes[4], 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
  for (unsigned i = 1; i < 5; ++i)
  {
    CHECK(checkDataRecord(state, &record, &buffer, &data));
    CHECK(record.sequence == i && record.offset == i &&
        record.length == 1);
    CHECK(*(const uint8_t *)data == bytes[i]);
    CHECK(releaseClientStream(state, &buffer));
  }
  CHECK(state->ops->dataEnd(state->clipboard, transfer, 5) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(checkDataRecord(state, &record, &buffer, &data));
  CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_END);
  CHECK(record.sequence == 5 && record.offset == 5 && record.size == 5);
  CHECK(releaseClientStream(state, &buffer));

  KVMFRClipboardMessage requestRecord;
  CHECK(offerAndRequest(state, claimRecord.generation,
        501, 5001, &requestRecord));
  atomic_store(&state->events.blockChunkOnce, true);
  KVMFRClipboardMessage wire =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
  wire.clipboardGeneration = 501;
  wire.transfer = requestRecord.transfer;
  wire.size     = 1;
  wire.format   = KVMFR_CLIPBOARD_FORMAT_TEXT;
  wire.flags    = KVMFR_CLIPBOARD_FLAG_BEGIN |
    KVMFR_CLIPBOARD_FLAG_END;
  wire.length   = 1;
  CHECK(postStreamRecord(state, &wire, bytes));
  CHECK(waitAtomic(state, &state->events.dataChunk, 1));
  CHECK(atomic_load(&state->events.dataEnd) == 0);
  CHECK(state->ops->dataReady(state->clipboard, 5001));
  CHECK(waitAtomic(state, &state->events.dataEnd, 1));
  CHECK(atomic_load(&state->events.dataChunk) == 2);
  return true;
}

static bool testCancel(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(prepareOutbound(state, 60, &claimRecord, 4));
  const LG_ClipboardRequest outbound = state->events.requestID;
  KVMFRClipboardMessage cancel =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_CANCEL, claimRecord.generation);
  cancel.clipboardGeneration = state->localClipboardGeneration;
  cancel.transfer            = outbound;
  cancel.format              = KVMFR_CLIPBOARD_FORMAT_TEXT;
  cancel.token               = LG_CLIPBOARD_CANCEL_ABORTED;
  CHECK(postRecord(state, &cancel, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.requestCancel, 1));
  CHECK(state->events.cancelID == outbound);

  KVMFRClipboardMessage requestRecord;
  CHECK(offerAndRequest(state, claimRecord.generation,
        601, 6001, &requestRecord));
  cancel.clipboardGeneration = 601;
  cancel.transfer = requestRecord.transfer;
  cancel.token = LG_CLIPBOARD_CANCEL_REPLACED;
  CHECK(postRecord(state, &cancel, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.dataCancel, 1));
  CHECK(state->events.cancelID == 6001);
  CHECK(state->events.cancelReason == LG_CLIPBOARD_CANCEL_REPLACED);

  KVMFRClipboardMessage outboundRequest =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST,
      claimRecord.generation);
  outboundRequest.clipboardGeneration = state->localClipboardGeneration;
  outboundRequest.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | 5;
  outboundRequest.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
  CHECK(postRecord(state, &outboundRequest,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.request, 2));
  CHECK(state->ops->dataCancel(state->clipboard,
        outboundRequest.transfer, LG_CLIPBOARD_CANCEL_ABORTED));
  KVMFRClipboardMessage wireCancel;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CANCEL, &wireCancel));
  CHECK(wireCancel.transfer == outboundRequest.transfer);
  CHECK(wireCancel.clipboardGeneration ==
      state->localClipboardGeneration);
  CHECK(wireCancel.format == KVMFR_CLIPBOARD_FORMAT_TEXT);
  CHECK(wireCancel.token == LG_CLIPBOARD_CANCEL_ABORTED);
  return true;
}

static bool testRestart(TestState * state)
{
  KVMFRClipboardMessage oldClaim;
  CHECK(claim(state, 70, false, &oldClaim));
  CHECK(own(state, 70, oldClaim.generation));
  const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
  CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
  KVMFRClipboardMessage oldOffer;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &oldOffer));

  CHECK(postStatus(state, 71, 0, 0, true, 0));
  KVMFRClipboardMessage newClaim;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, &newClaim));
  CHECK(newClaim.generation != oldClaim.generation);
  CHECK(newClaim.token == 71);
  KVMFRClipboardMessage replay;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &replay));
  CHECK(replay.generation == newClaim.generation);
  CHECK(replay.clipboardGeneration == oldOffer.clipboardGeneration);
  CHECK(own(state, 71, newClaim.generation));

  CHECK(postStatus(state, 71, 0, 0, true, 0));
  KVMFRClipboardMessage reclaimed;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, &reclaimed));
  CHECK(reclaimed.generation != newClaim.generation);
  CHECK(reclaimed.token == 71);
  return true;
}

static bool testFileLimits(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 75, false, &claimRecord));
  CHECK(own(state, 75, claimRecord.generation));

  const uint64_t localDataset = UINT64_C(0x1234000012340001);
  const uint64_t remoteDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x5678000056780001);
  CHECK(state->ops->offerFiles(state->clipboard, localDataset));
  KVMFRClipboardMessage wire;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &wire));
  CHECK(wire.clipboardGeneration == localDataset);

  KVMFRClipboardMessage offer =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation);
  offer.clipboardGeneration = remoteDataset;
  offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
  CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.notice, 1));
  CHECK(atomic_load(&state->events.fileOffer) == 1);
  CHECK(atomic_load(&state->events.fileOfferBeforeNotice));
  CHECK(state->events.fileDataset == remoteDataset);

  KVMFRClipboardMessage staleAcquire =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE,
      claimRecord.generation);
  staleAcquire.clipboardGeneration = localDataset + 1U;
  staleAcquire.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x98);
  staleAcquire.format = KVMFR_CLIPBOARD_FORMAT_FILES;
  CHECK(postRecord(state, &staleAcquire,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, &wire));
  CHECK(wire.clipboardGeneration == staleAcquire.clipboardGeneration);
  CHECK(wire.transfer == staleAcquire.transfer);
  CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_STALE);
  CHECK(atomic_load(&state->events.fileAcquire) == 0);

  KVMFRClipboardMessage staleRequest =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST,
      claimRecord.generation);
  staleRequest.clipboardGeneration = localDataset;
  staleRequest.transfer            = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x99);
  staleRequest.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  staleRequest.token               = KVMFR_CLIPBOARD_FILE_OP_LIST;
  CHECK(postRecord(state, &staleRequest,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL, &wire));
  CHECK(wire.clipboardGeneration == staleRequest.clipboardGeneration);
  CHECK(wire.transfer == staleRequest.transfer);
  CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_STALE);
  CHECK(atomic_load(&state->events.fileRequest) == 0);

  for (uint64_t i = 0; i < 4; ++i)
  {
    const uint64_t acquisition = UINT64_C(0x100) + i;
    CHECK(state->ops->fileAcquire(
          state->clipboard, remoteDataset, acquisition));
    CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &wire));
    CHECK(wire.clipboardGeneration == remoteDataset);
    CHECK(wire.transfer == acquisition);

    KVMFRClipboardMessage acquired =
      hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
        claimRecord.generation);
    acquired.clipboardGeneration = remoteDataset;
    acquired.transfer            = acquisition;
    acquired.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
    CHECK(postRecord(state, &acquired,
          KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
    CHECK(waitAtomic(state, &state->events.fileAcquired, (unsigned)i + 1));
    CHECK(state->events.fileError == LG_CLIPBOARD_FILE_ERROR_NONE);
  }

  for (uint64_t i = 0; i < 4; ++i)
  {
    const uint64_t acquisition = KVMFR_CLIPBOARD_TRANSFER_HELPER |
      (UINT64_C(0x200) + i);
    KVMFRClipboardMessage acquire =
      hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE,
        claimRecord.generation);
    acquire.clipboardGeneration = localDataset;
    acquire.transfer            = acquisition;
    acquire.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
    CHECK(postRecord(state, &acquire,
          KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
    CHECK(waitAtomic(state, &state->events.fileAcquire, (unsigned)i + 1));
    CHECK(state->events.fileDataset == localDataset);
    CHECK(state->events.fileAcquisition == acquisition);
    CHECK(state->ops->fileAcquired(state->clipboard,
          localDataset, acquisition, LG_CLIPBOARD_FILE_ERROR_NONE));
    CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, &wire));
    CHECK(wire.transfer == acquisition);
    CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_NONE);
  }

  CHECK(!state->ops->fileAcquire(
        state->clipboard, remoteDataset, UINT64_C(0x999)));
  KVMFRClipboardMessage overflowAcquire =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE,
      claimRecord.generation);
  overflowAcquire.clipboardGeneration = localDataset;
  overflowAcquire.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x999);
  overflowAcquire.format = KVMFR_CLIPBOARD_FORMAT_FILES;
  CHECK(postRecord(state, &overflowAcquire,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(readNonKeepalive(state, &wire));
  CHECK(wire.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED);
  CHECK(wire.transfer == overflowAcquire.transfer);
  CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE);
  CHECK(atomic_load(&state->events.fileAcquire) == 4);

  for (uint64_t i = 0; i < 16; ++i)
  {
    const LG_ClipboardFileRequest request =
    {
      .dataset   = remoteDataset,
      .request   = UINT64_C(0x1000) + i,
      .operation = LG_CLIPBOARD_FILE_LIST,
    };
    CHECK(state->ops->fileRequest(state->clipboard, &request));
    CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, &wire));
    CHECK(wire.clipboardGeneration == remoteDataset);
    CHECK(wire.transfer == request.request);
    CHECK(wire.token == KVMFR_CLIPBOARD_FILE_OP_LIST);
  }

  for (uint64_t i = 0; i < 16; ++i)
  {
    KVMFRClipboardMessage request =
      hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST,
        claimRecord.generation);
    request.clipboardGeneration = localDataset;
    request.transfer            = KVMFR_CLIPBOARD_TRANSFER_HELPER |
      (UINT64_C(0x2000) + i);
    request.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
    request.token               = KVMFR_CLIPBOARD_FILE_OP_LIST;
    CHECK(postRecord(state, &request,
          KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
    CHECK(waitAtomic(state, &state->events.fileRequest,
          (unsigned)i + 1));
    CHECK(state->events.fileRequestValue.dataset == localDataset);
    CHECK(state->events.fileRequestValue.request == request.transfer);
  }

  const LG_ClipboardFileRequest overflowRequest =
  {
    .dataset   = remoteDataset,
    .request   = UINT64_C(0x9999),
    .operation = LG_CLIPBOARD_FILE_LIST,
  };
  CHECK(!state->ops->fileRequest(state->clipboard, &overflowRequest));
  KVMFRClipboardMessage inboundOverflow =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST,
      claimRecord.generation);
  inboundOverflow.clipboardGeneration = localDataset;
  inboundOverflow.transfer            = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x9999);
  inboundOverflow.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  inboundOverflow.token               = KVMFR_CLIPBOARD_FILE_OP_LIST;
  CHECK(postRecord(state, &inboundOverflow,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(readNonKeepalive(state, &wire));
  CHECK(wire.type == KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL);
  CHECK(wire.transfer == inboundOverflow.transfer);
  CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE);
  CHECK(atomic_load(&state->events.fileRequest) == 16);
  return true;
}

static bool testFileOwnerLoss(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 79, false, &claimRecord));
  CHECK(own(state, 79, claimRecord.generation));

  const uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x7900000179000001);
  KVMFRClipboardMessage offer =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation);
  offer.clipboardGeneration = dataset;
  offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
  CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileOffer, 1));

  const uint64_t acquisition = UINT64_C(0x7901);
  CHECK(state->ops->fileAcquire(
        state->clipboard, dataset, acquisition));
  KVMFRClipboardMessage wire;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &wire));

  KVMFRClipboardMessage acquired =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
      claimRecord.generation);
  acquired.clipboardGeneration = dataset;
  acquired.transfer            = acquisition;
  acquired.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  CHECK(postRecord(state, &acquired,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileAcquired, 1));

  const LG_ClipboardFileRequest request =
  {
    .dataset   = dataset,
    .request   = UINT64_C(0x7902),
    .operation = LG_CLIPBOARD_FILE_LIST,
  };
  CHECK(state->ops->fileRequest(state->clipboard, &request));
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, &wire));

  const unsigned cancellations =
    atomic_load(&state->events.fileCancel);
  CHECK(postStatus(state, 80, 0, 0, true, 0));
  CHECK(waitAtomic(state, &state->events.fileCancel,
        cancellations + 2));
  CHECK(state->events.fileDataset == dataset);
  CHECK(state->events.fileAcquisition == acquisition);
  CHECK(state->events.fileError == LG_CLIPBOARD_FILE_ERROR_DISCONNECTED);
  return true;
}

static bool testMalformedFileData(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 81, false, &claimRecord));
  CHECK(own(state, 81, claimRecord.generation));

  const uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x8100000181000001);
  KVMFRClipboardMessage offer =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation);
  offer.clipboardGeneration = dataset;
  offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
  CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileOffer, 1));

  const uint64_t acquisition = UINT64_C(0x8101);
  CHECK(state->ops->fileAcquire(state->clipboard, dataset, acquisition));
  KVMFRClipboardMessage wire;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &wire));

  KVMFRClipboardMessage acquired =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
      claimRecord.generation);
  acquired.clipboardGeneration = dataset;
  acquired.transfer = acquisition;
  acquired.format = KVMFR_CLIPBOARD_FORMAT_FILES;
  CHECK(postRecord(state, &acquired,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileAcquired, 1));

  const LG_ClipboardFileRequest request =
  {
    .dataset   = dataset,
    .request   = UINT64_C(0x8102),
    .node      = 2,
    .length    = 4,
    .operation = LG_CLIPBOARD_FILE_READ,
  };
  CHECK(state->ops->fileRequest(state->clipboard, &request));
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, &wire));

  KVMFRClipboardMessage malformed =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_DATA,
      claimRecord.generation);
  malformed.clipboardGeneration = dataset;
  malformed.transfer            = request.request;
  malformed.offset              = 1;
  malformed.size                = request.length;
  malformed.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  malformed.flags               = KVMFR_CLIPBOARD_FLAG_BEGIN;
  malformed.length              = 1;
  malformed.token               = KVMFR_CLIPBOARD_FILE_OP_READ;
  const uint8_t byte = 0;
  const unsigned cancellations =
    atomic_load(&state->events.fileCancel);
  CHECK(postStreamRecord(state, &malformed, &byte));
  CHECK(waitAtomic(state, &state->events.fileCancel, cancellations + 1));
  CHECK(state->events.fileDataset == dataset);
  CHECK(state->events.fileAcquisition == request.request);
  CHECK(state->events.fileError == LG_CLIPBOARD_FILE_ERROR_INVALID);
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL, &wire));
  CHECK(wire.clipboardGeneration == dataset);
  CHECK(wire.transfer == request.request);
  CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_INVALID);
  CHECK(!state->ops->fileCancel(state->clipboard,
        dataset, request.request, LG_CLIPBOARD_FILE_ERROR_CANCELLED));

  malformed.transfer = request.request + 1U;
  CHECK(postStreamRecord(state, &malformed, &byte));
  CHECK(settleHostStream(state));
  CHECK(atomic_load(&state->events.fileCancel) == cancellations + 1);
  return true;
}

static bool testFileStream(TestState * state)
{
  static uint8_t bytes[KVMFR_CLIPBOARD_FILE_READ_BYTES];
  for (size_t i = 0; i < sizeof(bytes); ++i)
    bytes[i] = (uint8_t)(i * 131U + 17U);

  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 78, false, &claimRecord));
  CHECK(own(state, 78, claimRecord.generation));
  const uint64_t dataset = UINT64_C(0x7800000178000001);
  CHECK(state->ops->offerFiles(state->clipboard, dataset));
  KVMFRClipboardMessage wire;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &wire));

  const uint64_t acquisition = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x7801);
  KVMFRClipboardMessage acquire =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE,
      claimRecord.generation);
  acquire.clipboardGeneration = dataset;
  acquire.transfer            = acquisition;
  acquire.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  CHECK(postRecord(state, &acquire,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileAcquire, 1));
  CHECK(state->ops->fileAcquired(state->clipboard,
        dataset, acquisition, LG_CLIPBOARD_FILE_ERROR_NONE));
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, &wire));

  KVMFRClipboardMessage request =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST,
      claimRecord.generation);
  request.clipboardGeneration = dataset;
  request.transfer            = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x7802);
  request.offset              = 123;
  request.size                = 42;
  request.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  request.flags               = sizeof(bytes);
  request.token               = KVMFR_CLIPBOARD_FILE_OP_READ;
  CHECK(postRecord(state, &request,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileRequest, 1));
  const LG_ClipboardFileRequest descriptor =
    state->events.fileRequestValue;
  CHECK(descriptor.dataset == dataset);
  CHECK(descriptor.request == request.transfer);
  CHECK(descriptor.node == 42);
  CHECK(descriptor.offset == 123);
  CHECK(descriptor.length == sizeof(bytes));
  CHECK(descriptor.operation == LG_CLIPBOARD_FILE_READ);
  CHECK(state->ops->fileDataBegin(state->clipboard,
        &descriptor, sizeof(bytes)) == LG_CLIPBOARD_RESULT_ACCEPTED);

  for (unsigned i = 0; i < KVMFR_CLIPBOARD_STREAM_SLOT_COUNT; ++i)
  {
    const uint64_t offset =
      (uint64_t)i * KVMFR_CLIPBOARD_STREAM_SLOT_BYTES;
    CHECK(state->ops->fileDataChunk(state->clipboard,
          &descriptor, offset, bytes + offset,
          KVMFR_CLIPBOARD_STREAM_SLOT_BYTES,
          i + 1 == KVMFR_CLIPBOARD_STREAM_SLOT_COUNT) ==
        LG_CLIPBOARD_RESULT_ACCEPTED);
  }
  KVMFRClipboardMessage record;
  LGMPStreamBuffer buffer;
  const void * data = NULL;
  for (unsigned i = 0; i < KVMFR_CLIPBOARD_STREAM_SLOT_COUNT; ++i)
  {
    const uint64_t offset =
      (uint64_t)i * KVMFR_CLIPBOARD_STREAM_SLOT_BYTES;
    CHECK(checkFileDataRecord(state, KVMFR_CLIPBOARD_FILE_OP_READ,
          &record, &buffer, &data));
    CHECK(record.offset == offset && record.sequence == i);
    CHECK(record.length == KVMFR_CLIPBOARD_STREAM_SLOT_BYTES);
    if (i == 0)
    {
      CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_BEGIN);
      CHECK(record.size == sizeof(bytes));
    }
    else if (i + 1 == KVMFR_CLIPBOARD_STREAM_SLOT_COUNT)
    {
      CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_END);
      CHECK(record.size == sizeof(bytes));
    }
    else
    {
      CHECK(record.flags == 0);
      CHECK(record.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN);
    }
    CHECK(memcmp(data, bytes + offset,
          KVMFR_CLIPBOARD_STREAM_SLOT_BYTES) == 0);
    CHECK(releaseClientStream(state, &buffer));
  }

  CHECK(state->ops->fileDataEnd(state->clipboard,
        &descriptor, sizeof(bytes)) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(noClientControl(state));

  const unsigned requests = atomic_load(&state->events.fileRequest);
  request.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(0x7803);
  request.offset   = 0;
  request.size     = KVMFR_CLIPBOARD_FILE_ROOT_NODE;
  request.flags    = 0;
  request.token    = KVMFR_CLIPBOARD_FILE_OP_LIST;
  CHECK(postRecord(state, &request,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileRequest, requests + 1));
  const LG_ClipboardFileRequest empty = state->events.fileRequestValue;
  CHECK(empty.operation == LG_CLIPBOARD_FILE_LIST);
  CHECK(state->ops->fileDataBegin(state->clipboard,
        &empty, KVMFR_CLIPBOARD_SIZE_UNKNOWN) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(state->ops->fileDataEnd(state->clipboard,
        &empty, 0) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(checkFileDataRecord(state, KVMFR_CLIPBOARD_FILE_OP_LIST,
        &record, &buffer, &data));
  CHECK(record.flags == (KVMFR_CLIPBOARD_FLAG_BEGIN |
        KVMFR_CLIPBOARD_FLAG_END));
  CHECK(record.offset == 0 && record.sequence == 0 && record.size == 0);
  CHECK(record.length == 0);
  CHECK(releaseClientStream(state, &buffer));
  return true;
}

static void * disconnectThread(void * opaque)
{
  DisconnectTask * task = opaque;
  lgmpClipboard_disconnect(task->clipboard);
  atomic_store(&task->done, true);
  return NULL;
}

static bool disconnectAndDrain(TestState * state,
    uint32_t * releaseGeneration)
{
  DisconnectTask task = { .clipboard = state->clipboard };
  atomic_init(&task.done, false);
  pthread_t thread;
  CHECK(pthread_create(&thread, NULL, disconnectThread, &task) == 0);
  bool valid = true;
  for (unsigned i = 0; i < TEST_WAIT_MS && !atomic_load(&task.done); ++i)
  {
    if (lgmpHostProcess(state->host) != LGMP_OK)
      valid = false;
    for (;;)
    {
      KVMFRClipboardMessage record;
      size_t size = sizeof(record);
      uint32_t clientID = 0;
      const LGMP_STATUS status = lgmpHostReadDataWithSource(
        state->queue, &record, &size, &clientID);
      if (status == LGMP_ERR_QUEUE_EMPTY)
        break;
      if (status != LGMP_OK)
      {
        valid = false;
        break;
      }
      valid &= size == sizeof(record);
      valid &= clientID == state->clientID;
      if (record.type == KVMFR_CLIPBOARD_MESSAGE_RELEASE &&
          releaseGeneration)
        *releaseGeneration = record.generation;
      if (lgmpHostAckData(state->queue) != LGMP_OK)
        valid = false;
    }
    usleep(1000);
  }
  valid &= pthread_join(thread, NULL) == 0;
  valid &= atomic_load(&task.done);
  return valid;
}

static bool recreateClipboard(TestState * state)
{
  state->ops->setStatusListener(state->clipboard, NULL, NULL);
  if (atomic_load(&state->events.attached))
  {
    state->ops->detach(state->clipboard);
    atomic_store(&state->events.attached, false);
    KVMFRClipboardMessage release;
    CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_RELEASE, &release));
    CHECK(release.generation != 0);
    const uint32_t endpointGeneration =
      atomic_load(&state->statusGeneration);
    CHECK(endpointGeneration != 0);
    CHECK(postStatus(state, endpointGeneration, 0, 0, true, 0));
  }
  CHECK(disconnectAndDrain(state, NULL));
  lgmpClipboard_destroy(&state->clipboard);

  CHECK(lgmpClipboard_create(state->client, &state->clipboard));
  CHECK(lgmpClipboard_connect(state->clipboard, state->clientID));
  state->ops = lgmpClipboard_getOps();
  CHECK(state->ops);
  state->ops->setStatusListener(state->clipboard, statusChanged, state);
  for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
  {
    CHECK(hostProcess(state));
    if (lgmpHostQueueHasSubs(state->queue))
      return true;
    usleep(1000);
  }
  return false;
}

static bool testProcessRestart(TestState * state)
{
  KVMFRClipboardMessage oldClaim;
  CHECK(claim(state, 76, false, &oldClaim));
  CHECK(own(state, 76, oldClaim.generation));
  const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
  CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
  KVMFRClipboardMessage oldLocalOffer;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &oldLocalOffer));
  KVMFRClipboardMessage oldRequest;
  CHECK(offerAndRequest(state, oldClaim.generation,
        760, 7600, &oldRequest));
  const uint64_t oldFileDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x76000001);
  const uint64_t oldFileAcquisition = UINT64_C(0x76010001);
  KVMFRClipboardMessage fileOffer =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, oldClaim.generation);
  fileOffer.clipboardGeneration = oldFileDataset;
  fileOffer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
  const unsigned oldFileOffers = atomic_load(&state->events.fileOffer);
  CHECK(postRecord(state, &fileOffer,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileOffer, oldFileOffers + 1));
  CHECK(state->ops->fileAcquire(state->clipboard,
        oldFileDataset, oldFileAcquisition));
  KVMFRClipboardMessage fileWire;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &fileWire));

  CHECK(recreateClipboard(state));
  KVMFRClipboardMessage newClaim;
  CHECK(claim(state, 77, false, &newClaim));
  CHECK(own(state, 77, newClaim.generation));
  CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
  KVMFRClipboardMessage newLocalOffer;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &newLocalOffer));
  CHECK(newLocalOffer.clipboardGeneration !=
      oldLocalOffer.clipboardGeneration);
  KVMFRClipboardMessage newRequest;
  CHECK(offerAndRequest(state, newClaim.generation,
        770, 7700, &newRequest));
  CHECK(newRequest.transfer != oldRequest.transfer);

  const unsigned cancelCount = atomic_load(&state->events.dataCancel);
  KVMFRClipboardMessage staleCancel =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_CANCEL, newClaim.generation);
  staleCancel.clipboardGeneration = 760;
  staleCancel.transfer            = oldRequest.transfer;
  staleCancel.format              = KVMFR_CLIPBOARD_FORMAT_TEXT;
  staleCancel.token               = LG_CLIPBOARD_CANCEL_ABORTED;
  CHECK(postRecord(state, &staleCancel,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
  CHECK(atomic_load(&state->events.dataCancel) == cancelCount);

  const unsigned beginCount = atomic_load(&state->events.dataBegin);
  KVMFRClipboardMessage staleData =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, newClaim.generation);
  staleData.clipboardGeneration = 760;
  staleData.transfer            = oldRequest.transfer;
  staleData.format              = KVMFR_CLIPBOARD_FORMAT_TEXT;
  staleData.flags               = KVMFR_CLIPBOARD_FLAG_BEGIN |
    KVMFR_CLIPBOARD_FLAG_END;
  CHECK(postStreamRecord(state, &staleData, NULL));
  CHECK(settleHostStream(state));
  CHECK(atomic_load(&state->events.dataBegin) == beginCount);

  KVMFRClipboardMessage data =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, newClaim.generation);
  data.clipboardGeneration = 770;
  data.transfer            = newRequest.transfer;
  data.format              = KVMFR_CLIPBOARD_FORMAT_TEXT;
  data.flags               = KVMFR_CLIPBOARD_FLAG_BEGIN |
    KVMFR_CLIPBOARD_FLAG_END;
  const unsigned endCount = atomic_load(&state->events.dataEnd);
  CHECK(postStreamRecord(state, &data, NULL));
  CHECK(waitAtomic(state, &state->events.dataEnd, endCount + 1));
  CHECK(atomic_load(&state->events.dataBegin) == beginCount + 1);

  const uint64_t newFileDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
    UINT64_C(0x77000001);
  const uint64_t newFileAcquisition = UINT64_C(0x77010001);
  fileOffer = hostRecord(
      KVMFR_CLIPBOARD_MESSAGE_OFFER, newClaim.generation);
  fileOffer.clipboardGeneration = newFileDataset;
  fileOffer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
  const unsigned newFileOffers = atomic_load(&state->events.fileOffer);
  CHECK(postRecord(state, &fileOffer,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileOffer, newFileOffers + 1));
  CHECK(state->ops->fileAcquire(state->clipboard,
        newFileDataset, newFileAcquisition));
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &fileWire));

  const unsigned acquiredCount =
    atomic_load(&state->events.fileAcquired);
  KVMFRClipboardMessage staleAcquired =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
      newClaim.generation);
  staleAcquired.clipboardGeneration = oldFileDataset;
  staleAcquired.transfer            = oldFileAcquisition;
  staleAcquired.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  CHECK(postRecord(state, &staleAcquired,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
  CHECK(atomic_load(&state->events.fileAcquired) == acquiredCount);

  KVMFRClipboardMessage staleFileCancel =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL,
      newClaim.generation);
  staleFileCancel.clipboardGeneration = oldFileDataset;
  staleFileCancel.transfer            = oldFileAcquisition;
  staleFileCancel.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  staleFileCancel.token               = KVMFR_CLIPBOARD_FILE_ERROR_STALE;
  CHECK(postRecord(state, &staleFileCancel,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
  CHECK(atomic_load(&state->events.fileCancel) == 0);

  KVMFRClipboardMessage acquired =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
      newClaim.generation);
  acquired.clipboardGeneration = newFileDataset;
  acquired.transfer            = newFileAcquisition;
  acquired.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  CHECK(postRecord(state, &acquired,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.fileAcquired,
        acquiredCount + 1));
  CHECK(state->events.fileDataset == newFileDataset);
  CHECK(state->events.fileAcquisition == newFileAcquisition);
  return true;
}

static bool testDisconnect(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 80, false, &claimRecord));
  CHECK(own(state, 80, claimRecord.generation));
  uint32_t releaseGeneration = 0;
  CHECK(disconnectAndDrain(state, &releaseGeneration));
  CHECK(releaseGeneration == claimRecord.generation);
  return true;
}

static bool testMalformed(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 90, false, &claimRecord));
  CHECK(own(state, 90, claimRecord.generation));

  const unsigned statusCount = atomic_load(&state->statusCount);
  PLGMPMemory badStatusMemory;
  CHECK(allocMemory(state, sizeof(KVMFRClipboardStatus),
        &badStatusMemory));
  KVMFRClipboardStatus * badStatus = lgmpHostMemPtr(badStatusMemory);
  *badStatus = (KVMFRClipboardStatus)
  {
    .version    = KVMFR_CLIPBOARD_VERSION,
    .flags      = KVMFR_CLIPBOARD_STATUS_AVAILABLE,
    .generation = 91,
    .lease      = 1000,
  };
  CHECK(postMemory(state, badStatusMemory,
        KVMFR_CLIPBOARD_QUEUE_STATUS, nextSerial(state)));
  CHECK(waitMemory(state, badStatusMemory));
  CHECK(atomic_load(&state->statusCount) == statusCount);

  KVMFRClipboardMessage malformed =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation + 1);
  malformed.clipboardGeneration = 900;
  malformed.token = KVMFR_CLIPBOARD_FORMAT_MASK_TEXT;
  CHECK(postRecord(state, &malformed,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
  CHECK(atomic_load(&state->events.notice) == 0);

  malformed.generation = claimRecord.generation;
  CHECK(postStreamRecord(state, &malformed, NULL));
  CHECK(settleHostStream(state));
  CHECK(atomic_load(&state->events.notice) == 0);

  KVMFRClipboardMessage requestRecord;
  CHECK(offerAndRequest(state, claimRecord.generation,
        901, 9001, &requestRecord));
  KVMFRClipboardMessage data =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
  data.clipboardGeneration = 901;
  data.transfer = requestRecord.transfer;
  data.offset   = 1;
  data.size     = 1;
  data.format   = KVMFR_CLIPBOARD_FORMAT_TEXT;
  data.flags    = KVMFR_CLIPBOARD_FLAG_BEGIN |
    KVMFR_CLIPBOARD_FLAG_END;
  CHECK(postStreamRecord(state, &data, NULL));
  CHECK(settleHostStream(state));
  CHECK(atomic_load(&state->events.dataBegin) == 0);

  const uint8_t byte = 42;
  data.offset = 0;
  data.length = 1;
  CHECK(postStreamRecord(state, &data, &byte));
  CHECK(waitAtomic(state, &state->events.dataEnd, 1));
  CHECK(atomic_load(&state->events.dataBegin) == 1);
  CHECK(atomic_load(&state->events.dataChunk) == 1);
  CHECK(state->events.streamID == 9001);
  CHECK(state->events.chunkData[0] == byte);
  return true;
}

static bool testFileFormatIsolation(TestState * state)
{
  KVMFRClipboardMessage claimRecord;
  CHECK(claim(state, 91, false, &claimRecord));
  CHECK(own(state, 91, claimRecord.generation));

  const LG_ClipboardData files[] = { LG_CLIPBOARD_DATA_FILES };
  CHECK(!state->ops->notifyTypes(state->clipboard, files, 1));
  CHECK(noClientControl(state));

  const uint64_t localDataset = UINT64_C(0x123456789abcdef);
  CHECK(state->ops->offerFiles(state->clipboard, localDataset));
  KVMFRClipboardMessage localOffer;
  CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &localOffer));
  CHECK(localOffer.clipboardGeneration == localDataset);

  KVMFRClipboardMessage request =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST, claimRecord.generation);
  request.clipboardGeneration = localDataset;
  request.transfer            = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1);
  request.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  CHECK(postRecord(state, &request, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
  CHECK(atomic_load(&state->events.request) == 0);
  CHECK(state->ops->dataBegin(state->clipboard, request.transfer,
        LG_CLIPBOARD_DATA_FILES, 0) == LG_CLIPBOARD_RESULT_FAILED);

  KVMFRClipboardMessage remoteOffer =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation);
  remoteOffer.clipboardGeneration = UINT64_C(0x2222333344445555);
  remoteOffer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
  CHECK(postRecord(state, &remoteOffer,
        KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
  CHECK(waitAtomic(state, &state->events.notice, 1));
  CHECK(state->events.noticeCount == 1);
  CHECK(state->events.noticeTypes[0] == LG_CLIPBOARD_DATA_FILES);
  CHECK(!state->ops->request(state->clipboard,
        100, LG_CLIPBOARD_DATA_FILES));
  CHECK(noClientControl(state));

  KVMFRClipboardMessage data =
    hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
  data.clipboardGeneration = remoteOffer.clipboardGeneration;
  data.transfer            = UINT64_C(0x2345);
  data.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  data.flags               = KVMFR_CLIPBOARD_FLAG_BEGIN |
    KVMFR_CLIPBOARD_FLAG_END;
  CHECK(postStreamRecord(state, &data, NULL));
  CHECK(settleHostStream(state));
  CHECK(atomic_load(&state->events.dataBegin) == 0);
  return true;
}

static bool stateInit(TestState * state)
{
  memset(state, 0, sizeof(*state));
  state->memory = MAP_FAILED;
  atomic_init(&state->statusCount, 0);
  atomic_init(&state->statusAvailable, false);
  atomic_init(&state->statusGeneration, 0);
  atomic_init(&state->autoAttach, false);
  atomic_init(&state->events.notice, 0);
  atomic_init(&state->events.release, 0);
  atomic_init(&state->events.request, 0);
  atomic_init(&state->events.requestCancel, 0);
  atomic_init(&state->events.dataBegin, 0);
  atomic_init(&state->events.dataChunk, 0);
  atomic_init(&state->events.dataEnd, 0);
  atomic_init(&state->events.dataCancel, 0);
  atomic_init(&state->events.dataReady, 0);
  atomic_init(&state->events.fileOffer, 0);
  atomic_init(&state->events.fileAcquire, 0);
  atomic_init(&state->events.fileAcquired, 0);
  atomic_init(&state->events.fileRequest, 0);
  atomic_init(&state->events.fileCancel, 0);
  atomic_init(&state->events.acceptRequest, true);
  atomic_init(&state->events.blockChunkOnce, false);
  atomic_init(&state->events.attached, false);
  atomic_init(&state->events.fileOfferBeforeNotice, false);

  state->memory = mmap(NULL, TEST_SHM_SIZE, PROT_READ | PROT_WRITE,
    MAP_SHARED | MAP_ANONYMOUS, -1, 0);
  CHECK(state->memory != MAP_FAILED);
  const uint32_t sessionData = UINT32_C(0x12345678);
  CHECK(lgmpHostInit(state->memory, TEST_SHM_SIZE, &state->host,
        sizeof(sessionData), (uint8_t *)&sessionData) == LGMP_OK);
  const struct LGMPQueueConfig config =
  {
    .queueID     = LGMP_Q_CLIPBOARD,
    .numMessages = LGMP_Q_CLIPBOARD_LEN,
    .subTimeout  = 1000,
  };
  CHECK(lgmpHostQueueNew(state->host, config, &state->queue) == LGMP_OK);
  CHECK(lgmpClientInit(state->memory, TEST_SHM_SIZE, &state->client) ==
      LGMP_OK);
  usleep(300000);
  CHECK(hostProcess(state));
  uint32_t dataSize;
  uint32_t remoteVersion;
  uint8_t * data;
  CHECK(lgmpClientSessionInit(state->client, &dataSize, &data,
        &state->clientID, &remoteVersion) == LGMP_OK);
  CHECK(state->clientID != 0);
  CHECK(dataSize == sizeof(sessionData));
  CHECK(memcmp(data, &sessionData, sizeof(sessionData)) == 0);

  const struct LGMPStreamConfig hostToClientConfig =
  {
    .direction = LGMP_STREAM_HOST_TO_CLIENT,
    .policy    = LGMP_STREAM_RELIABLE_FIFO,
    .slotCount = KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
    .slotSize  = SLOT_BYTES,
  };
  CHECK(lgmpHostStreamNew(state->host, hostToClientConfig,
        &state->hostToClientStream) == LGMP_OK);
  struct LGMPStreamDescriptor descriptor;
  lgmpHostStreamGetDescriptor(state->hostToClientStream, &descriptor);
  state->hostToClientDescriptor = exportStreamDescriptor(&descriptor);

  const struct LGMPStreamConfig clientToHostConfig =
  {
    .direction = LGMP_STREAM_CLIENT_TO_HOST,
    .policy    = LGMP_STREAM_RELIABLE_FIFO,
    .slotCount = KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
    .slotSize  = SLOT_BYTES,
  };
  CHECK(lgmpHostStreamNew(state->host, clientToHostConfig,
        &state->clientToHostStream) == LGMP_OK);
  lgmpHostStreamGetDescriptor(state->clientToHostStream, &descriptor);
  state->clientToHostDescriptor = exportStreamDescriptor(&descriptor);

  CHECK(lgmpClipboard_create(state->client, &state->clipboard));
  CHECK(lgmpClipboard_connect(state->clipboard, state->clientID));
  state->ops = lgmpClipboard_getOps();
  CHECK(state->ops);
  state->ops->setStatusListener(
    state->clipboard, statusChanged, state);
  CHECK(atomic_load(&state->statusCount) == 1);
  CHECK(!atomic_load(&state->statusAvailable));

  for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
  {
    CHECK(hostProcess(state));
    if (lgmpHostQueueHasSubs(state->queue))
      return true;
    usleep(1000);
  }
  return false;
}

static void stateFree(TestState * state)
{
  if (state->clipboard)
  {
    state->ops->setStatusListener(state->clipboard, NULL, NULL);
    if (atomic_load(&state->events.attached))
      state->ops->detach(state->clipboard);
    disconnectAndDrain(state, NULL);
    lgmpClipboard_destroy(&state->clipboard);
  }
  lgmpClientFree(&state->client);
  for (unsigned i = 0; i < state->memoryCount; ++i)
    lgmpHostMemFree(&state->memories[i]);
  lgmpHostStreamFree(&state->clientToHostStream);
  lgmpHostStreamFree(&state->hostToClientStream);
  lgmpHostFree(&state->host);
  if (state->memory != MAP_FAILED)
    munmap(state->memory, TEST_SHM_SIZE);
}

typedef bool (*TestFn)(TestState * state);

static const struct
{
  const char * name;
  TestFn run;
}
TESTS[] =
{
  { "claim"           , testClaim               },
  { "offer-request"   , testOfferRequest        },
  { "inbound-stream"  , testInboundStream       },
  { "outbound-stream" , testOutboundStream      },
  { "blocked"         , testBlocked             },
  { "cancel"          , testCancel              },
  { "restart"         , testRestart             },
  { "process-restart" , testProcessRestart      },
  { "file-stream"     , testFileStream          },
  { "file-limits"     , testFileLimits          },
  { "file-owner-loss" , testFileOwnerLoss       },
  { "file-malformed"  , testMalformedFileData   },
  { "disconnect"      , testDisconnect          },
  { "malformed"       , testMalformed           },
  { "file-format"     , testFileFormatIsolation },
};

int main(int argc, char * argv[])
{
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s <test-case>\n", argv[0]);
    return 2;
  }
  TestFn test = NULL;
  for (unsigned i = 0; i < sizeof(TESTS) / sizeof(TESTS[0]); ++i)
    if (strcmp(argv[1], TESTS[i].name) == 0)
    {
      test = TESTS[i].run;
      break;
    }
  if (!test)
  {
    fprintf(stderr, "unknown test case: %s\n", argv[1]);
    return 2;
  }

  debug_init();
  TestState state = { 0 };
  state.memory = MAP_FAILED;
  const bool initialized = stateInit(&state);
  const bool passed = initialized && test(&state);
  stateFree(&state);
  return passed ? 0 : 1;
}
