/**
 * 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 "atoms.h"
#include "clipboard.h"
#include "test.h"
#include "x11.h"
#include "core/clipboard.h"
#include "core/clipboard_files.h"

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

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>

#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0]))
#define MAX_ABORT    12U
#define MAX_CHANGE   8U
#define MAX_CONVERT  16U
#define MAX_DATA     4U
#define MAX_NOTICE   8U
#define MAX_PROP     16U
#define MAX_SEND     8U
#define MAX_TRANSFER (128U * 1024U)
#define MAX_WINDOW   16U

#define A_CLIPBOARD  10UL
#define A_TARGETS    11UL
#define A_SEL_DATA   12UL
#define A_INCR       13UL
#define A_TEXT       100UL
#define A_PNG        101UL
#define A_BMP        102UL
#define A_TIFF       103UL
#define A_JPEG       104UL
#define A_URI        105UL
#define A_GNOME      106UL
#define A_MATE       107UL
#define A_KDE        108UL
#define A_TEXT_PLAIN 109UL
#define A_TEXT_UTF8  110UL

struct WindowLog
{
  Window window;
  bool   dead;
};

struct ConvertLog
{
  Atom   selection;
  Atom   target;
  Atom   property;
  Window requestor;
};

struct ChangeLog
{
  Window        window;
  Atom          property;
  Atom          type;
  int           format;
  int           count;
  unsigned char data[MAX_TRANSFER];
  size_t        size;
};

struct Property
{
  int           status;
  Atom          type;
  int           format;
  unsigned long count;
  unsigned long after;
  unsigned char data[MAX_TRANSFER];
  size_t        size;
  bool          null;
};

struct TypeNotice
{
  LG_ClipboardData type[LG_CLIPBOARD_DATA_NONE];
  size_t           count;
};

struct DataNotice
{
  LG_ClipboardRequest request;
  LG_ClipboardData    type;
  uint8_t             data[MAX_TRANSFER];
  size_t              size;
};

struct Log
{
  struct WindowLog   window[MAX_WINDOW];
  struct ConvertLog  convert[MAX_CONVERT];
  struct ChangeLog   change[MAX_CHANGE];
  struct Property    prop[MAX_PROP];
  struct TypeNotice  notice[MAX_NOTICE];
  struct DataNotice  data[MAX_DATA];
  XEvent             send[MAX_SEND];
  LG_ClipboardRequest abort[MAX_ABORT];
  unsigned int       windowN;
  unsigned int       convertN;
  unsigned int       changeN;
  unsigned int       propN;
  unsigned int       propPos;
  unsigned int       noticeN;
  unsigned int       dataN;
  unsigned int       sendN;
  unsigned int       abortN;
  unsigned int       releaseN;
  unsigned int       requestN;
  unsigned int       requestReadyN;
  unsigned int       destroyN;
  unsigned int       freeN;
  unsigned int       flushN;
  unsigned int       grabN;
  unsigned int       ungrabN;
  unsigned int       selectN;
  unsigned int       structureSelectN;
  LG_ClipboardData   requestType;
  const LG_ClipboardStreamOps * requestStream;
  void              * requestOpaque;
  LG_ClipboardRequest requestId;
  LG_ClipboardRequest streamRequest;
  LG_ClipboardData    streamType;
  uint64_t            streamSizeHint;
  unsigned int        streamBeginN;
  unsigned int        streamChunkN;
  unsigned int        streamEndN;
  size_t              streamMaxChunk;
  LG_ClipboardResult  beginResult;
  LG_ClipboardResult  chunkResult;
  LG_ClipboardResult  endResult;
  LG_ClipboardResult  requestBeginResult;
  bool                readyDuringChunk;
  bool                readyDuringEnd;
  Window              owner;
  Window              nextWindow;
  bool                requestOK;
  bool                requestCancelSync;
  bool                requestBeginSync;
  bool                fixesOK;
  bool                fileSetOK;
  bool                presentationOK;
  unsigned int        fileSetN;
  unsigned int        fileClearN;
  unsigned int        presentationAcquireN;
  unsigned int        presentationDeliveredN;
  unsigned int        presentationReleaseN;
  uint64_t            presentationDelivered[8];
  uint64_t            presentationReleased[8];
  char                fileMime[80];
  uint8_t             fileData[MAX_TRANSFER];
  size_t              fileSize;
};

struct X11DSState x11;
struct X11DSAtoms x11atoms;

static struct Log rec;

static pthread_mutex_t readRaceLock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  readRaceCond = PTHREAD_COND_INITIALIZER;
static bool readRacePause;
static bool readRaceEntered;
static bool readRaceResume;

static size_t itemSize(int format, unsigned long count)
{
  switch (format)
  {
    case 8:
      return count;
    case 16:
      return count * sizeof(short);
    case 32:
      return count * sizeof(long);
    default:
      return 0;
  }
}

Atom XInternAtom(Display * display, const char * name, Bool onlyIfExists)
{
  CHECK(display == x11.display);
  CHECK(!onlyIfExists);
  if (!strcmp(name, "UTF8_STRING"))
    return A_TEXT;
  if (!strcmp(name, "text/plain"))
    return A_TEXT_PLAIN;
  if (!strcmp(name, "text/plain;charset=utf-8"))
    return A_TEXT_UTF8;
  if (!strcmp(name, "image/png"))
    return A_PNG;
  if (!strcmp(name, "image/bmp"))
    return A_BMP;
  if (!strcmp(name, "image/tiff"))
    return A_TIFF;
  if (!strcmp(name, "image/jpeg"))
    return A_JPEG;
  if (!strcmp(name, "text/uri-list"))
    return A_URI;
  if (!strcmp(name, "x-special/gnome-copied-files"))
    return A_GNOME;
  if (!strcmp(name, "x-special/mate-copied-files"))
    return A_MATE;
  if (!strcmp(name, "application/x-kde-cutselection"))
    return A_KDE;
  CHECK(false);
  return None;
}

Bool XFixesQueryExtension(Display * display, int * eventBase, int * errorBase)
{
  CHECK(display == x11.display);
  if (!rec.fixesOK)
    return False;
  *eventBase = 80;
  *errorBase = 81;
  return True;
}

void XFixesSelectSelectionInput(Display * display, Window window,
    Atom selection, unsigned long mask)
{
  CHECK(display == x11.display);
  CHECK(window == x11.window);
  CHECK(selection == x11atoms.CLIPBOARD);
  CHECK(mask == XFixesSetSelectionOwnerNotifyMask);
  ++rec.selectN;
}

Window XCreateSimpleWindow(Display * display, Window parent,
    int x, int y, unsigned int width, unsigned int height,
    unsigned int borderWidth, unsigned long border,
    unsigned long background)
{
  CHECK(display == x11.display);
  CHECK(parent == x11.window);
  CHECK(x == 0);
  CHECK(y == 0);
  CHECK(width == 1);
  CHECK(height == 1);
  CHECK(borderWidth == 0);
  CHECK(border == 0);
  CHECK(background == 0);
  CHECK(rec.windowN < ARRAY_LENGTH(rec.window));
  struct WindowLog * window = &rec.window[rec.windowN++];
  window->window = ++rec.nextWindow;
  return window->window;
}

int XDestroyWindow(Display * display, Window window)
{
  CHECK(display == x11.display);
  for (unsigned int i = 0; i < rec.windowN; ++i)
    if (rec.window[i].window == window)
    {
      CHECK(!rec.window[i].dead);
      rec.window[i].dead = true;
      ++rec.destroyN;
      return Success;
    }
  CHECK(false);
  return BadWindow;
}

int XConvertSelection(Display * display, Atom selection, Atom target,
    Atom property, Window requestor, Time time)
{
  CHECK(display == x11.display);
  CHECK(time == CurrentTime);
  CHECK(rec.convertN < ARRAY_LENGTH(rec.convert));
  struct ConvertLog * convert = &rec.convert[rec.convertN++];
  convert->selection = selection;
  convert->target    = target;
  convert->property  = property;
  convert->requestor = requestor;
  return Success;
}

int XSelectInput(Display * display, Window window, long mask)
{
  CHECK(display == x11.display);
  CHECK(mask == PropertyChangeMask ||
      mask == (PropertyChangeMask | StructureNotifyMask));
  if (mask & StructureNotifyMask)
    ++rec.structureSelectN;
  CHECK(window != None);
  return Success;
}

int XSetSelectionOwner(Display * display, Atom selection,
    Window owner, Time time)
{
  CHECK(display == x11.display);
  CHECK(selection == x11atoms.CLIPBOARD);
  CHECK(time == CurrentTime);
  rec.owner = owner;
  return Success;
}

Window XGetSelectionOwner(Display * display, Atom selection)
{
  CHECK(display == x11.display);
  CHECK(selection == x11atoms.CLIPBOARD);
  return rec.owner;
}

int XGrabServer(Display * display)
{
  CHECK(display == x11.display);
  ++rec.grabN;
  return Success;
}

int XUngrabServer(Display * display)
{
  CHECK(display == x11.display);
  ++rec.ungrabN;
  return Success;
}

int XFlush(Display * display)
{
  CHECK(display == x11.display);
  ++rec.flushN;
  return Success;
}

int XChangeProperty(Display * display, Window window, Atom property,
    Atom type, int format, int mode, const unsigned char * data, int count)
{
  CHECK(display == x11.display);
  CHECK(mode == PropModeReplace);
  CHECK(count >= 0);
  CHECK(rec.changeN < ARRAY_LENGTH(rec.change));
  struct ChangeLog * change = &rec.change[rec.changeN++];
  change->window   = window;
  change->property = property;
  change->type     = type;
  change->format   = format;
  change->count    = count;
  change->size     = itemSize(format, count);
  CHECK(change->size <= sizeof(change->data));
  CHECK(!change->size || data);
  if (change->size)
    memcpy(change->data, data, change->size);
  return Success;
}

Status XSendEvent(Display * display, Window window, Bool propagate,
    long mask, XEvent * event)
{
  CHECK(display == x11.display);
  CHECK(!propagate);
  CHECK(mask == 0);
  CHECK(rec.sendN < ARRAY_LENGTH(rec.send));
  CHECK(event->xselection.requestor == window);
  rec.send[rec.sendN++] = *event;
  return True;
}

int XGetWindowProperty(Display * display, Window window, Atom property,
    long offset, long length, Bool delete, Atom requestedType,
    Atom * actualType, int * actualFormat, unsigned long * count,
    unsigned long * after, unsigned char ** data)
{
  CHECK(display == x11.display);
  CHECK(window != None);
  CHECK(property != None);
  CHECK(offset >= 0);
  CHECK(length == ~0L || length ==
    (KVMFR_CLIPBOARD_REPRESENTATION_BYTES + 3U) / 4U);
  CHECK(delete);
  CHECK(requestedType == AnyPropertyType);
  CHECK(rec.propPos < rec.propN);
  const struct Property * prop = &rec.prop[rec.propPos];
  *actualType   = prop->type;
  *actualFormat = prop->format;
  size_t start = 0;
  size_t size = prop->size;
  if (prop->format == 8 && length != ~0L)
  {
    start = (size_t)offset * 4U;
    CHECK(start <= prop->size);
    const size_t maximum = (size_t)length * 4U;
    size = prop->size - start;
    if (size > maximum)
      size = maximum;
    *count = size;
    *after = prop->size - start - size;
  }
  else
  {
    *count = prop->count;
    *after = prop->after;
  }
  if (prop->null)
    *data = NULL;
  else
  {
    CHECK(start + size <= sizeof(prop->data));
    *data = malloc(size ? size : 1);
    CHECK(*data);
    if (size)
      memcpy(*data, prop->data + start, size);
  }
  if (*after == 0)
    ++rec.propPos;
  return prop->status;
}

int XFree(void * data)
{
  CHECK(data);
  ++rec.freeN;
  free(data);
  return Success;
}

void clipboard_notifyTypes(
    const LG_ClipboardData types[], size_t count)
{
  CHECK(rec.noticeN < ARRAY_LENGTH(rec.notice));
  CHECK(count <= ARRAY_LENGTH(rec.notice[0].type));
  struct TypeNotice * notice = &rec.notice[rec.noticeN++];
  notice->count = count;
  memcpy(notice->type, types, count * sizeof(*types));
}

void clipboard_data(LG_ClipboardRequest request,
    LG_ClipboardData type, const void * data, size_t size)
{
  CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
  CHECK(size <= MAX_TRANSFER);
  CHECK(!size || data);
  struct DataNotice * notice = &rec.data[rec.dataN++];
  notice->request = request;
  notice->type    = type;
  notice->size    = size;
  if (size)
    memcpy(notice->data, data, size);
}

LG_ClipboardResult clipboard_dataBegin(LG_ClipboardRequest request,
    LG_ClipboardData type, uint64_t sizeHint)
{
  ++rec.streamBeginN;
  rec.streamRequest  = request;
  rec.streamType     = type;
  rec.streamSizeHint = sizeHint;
  if (rec.beginResult == LG_CLIPBOARD_RESULT_ACCEPTED)
  {
    CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
    rec.data[rec.dataN] = (struct DataNotice)
    {
      .request = request,
      .type    = type,
    };
  }
  return rec.beginResult;
}

LG_ClipboardResult clipboard_dataChunk(LG_ClipboardRequest request,
    uint64_t offset, const void * data, size_t size)
{
  const LG_ClipboardResult result = rec.chunkResult;
  ++rec.streamChunkN;
  CHECK(request == rec.streamRequest);
  CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
  struct DataNotice * notice = &rec.data[rec.dataN];
  CHECK(offset == notice->size);
  CHECK(data);
  CHECK(size);
  CHECK(size <= sizeof(notice->data) - notice->size);
  if (size > rec.streamMaxChunk)
    rec.streamMaxChunk = size;
  pthread_mutex_lock(&readRaceLock);
  if (readRacePause)
  {
    readRacePause   = false;
    readRaceEntered = true;
    pthread_cond_signal(&readRaceCond);
    while (!readRaceResume)
      pthread_cond_wait(&readRaceCond, &readRaceLock);
  }
  pthread_mutex_unlock(&readRaceLock);
  if (rec.readyDuringChunk)
  {
    rec.readyDuringChunk = false;
    rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
    x11CBRequestReady(request);
  }
  if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
  {
    memcpy(notice->data + notice->size, data, size);
    notice->size += size;
  }
  return result;
}

LG_ClipboardResult clipboard_dataEnd(LG_ClipboardRequest request,
    uint64_t finalSize)
{
  const LG_ClipboardResult result = rec.endResult;
  ++rec.streamEndN;
  CHECK(request == rec.streamRequest);
  CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
  if (rec.readyDuringEnd)
  {
    rec.readyDuringEnd = false;
    rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
    x11CBRequestReady(request);
  }
  if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
  {
    CHECK(finalSize == rec.data[rec.dataN].size);
    ++rec.dataN;
  }
  return result;
}

void clipboard_abort(LG_ClipboardRequest request)
{
  CHECK(rec.abortN < ARRAY_LENGTH(rec.abort));
  rec.abort[rec.abortN++] = request;
}

void clipboard_release(void)
{
  ++rec.releaseN;
}

bool clipboard_requestStream(LG_ClipboardData type,
    const LG_ClipboardStreamOps * stream, void * opaque,
    LG_ClipboardRequest * request)
{
  ++rec.requestN;
  rec.requestType = type;
  rec.requestStream = stream;
  rec.requestOpaque = opaque;
  rec.requestId = 9000 + rec.requestN;
  if (request)
    *request = rec.requestId;
  if (rec.requestBeginSync)
    rec.requestBeginResult = stream->begin(
        opaque, type, LG_CLIPBOARD_SIZE_UNKNOWN);
  if (rec.requestCancelSync)
    stream->cancel(opaque, LG_CLIPBOARD_CANCEL_REPLACED);
  return rec.requestOK;
}

bool clipboard_requestReady(LG_ClipboardRequest request)
{
  CHECK(request == rec.requestId);
  ++rec.requestReadyN;
  return true;
}

bool clipboardFiles_setLocal(const char * mime,
    const void * data, size_t size)
{
  CHECK(mime);
  CHECK(!size || data);
  CHECK(size <= sizeof(rec.fileData));
  ++rec.fileSetN;
  snprintf(rec.fileMime, sizeof(rec.fileMime), "%s", mime);
  rec.fileSize = size;
  if (size)
    memcpy(rec.fileData, data, size);
  return rec.fileSetOK;
}

void clipboardFiles_clearLocal(void)
{
  ++rec.fileClearN;
}

uint64_t clipboardFiles_remotePresentationAcquire(void)
{
  ++rec.presentationAcquireN;
  return rec.presentationOK ? 2001U : 0;
}

bool clipboardFiles_getRemotePresentation(uint64_t presentation,
    const char * mime, char ** data, size_t * size)
{
  CHECK(presentation >= 2001U);
  const char * value;
  if (!strcmp(mime, "text/uri-list"))
    value = "file:///run/user/1000/looking-glass/guest.txt\r\n";
  else if (!strcmp(mime, "x-special/gnome-copied-files") ||
      !strcmp(mime, "x-special/mate-copied-files"))
    value = "copy\nfile:///run/user/1000/looking-glass/guest.txt\r\n";
  else if (!strcmp(mime, "application/x-kde-cutselection"))
    value = "0";
  else
    return false;
  *size = strlen(value);
  *data = malloc(*size);
  CHECK(*data);
  memcpy(*data, value, *size);
  return true;
}

void clipboardFiles_remotePresentationDelivered(uint64_t presentation)
{
  CHECK(presentation);
  CHECK(rec.presentationDeliveredN <
      ARRAY_LENGTH(rec.presentationDelivered));
  rec.presentationDelivered[rec.presentationDeliveredN++] = presentation;
}

void clipboardFiles_remotePresentationRelease(uint64_t presentation)
{
  CHECK(presentation);
  CHECK(rec.presentationReleaseN <
      ARRAY_LENGTH(rec.presentationReleased));
  rec.presentationReleased[rec.presentationReleaseN++] = presentation;
}

static struct Property * prop(void)
{
  CHECK(rec.propN < ARRAY_LENGTH(rec.prop));
  struct Property * prop = &rec.prop[rec.propN++];
  prop->status = Success;
  return prop;
}

static void prop8(Atom type, const void * data, size_t size)
{
  struct Property * value = prop();
  CHECK(size <= sizeof(value->data));
  value->type   = type;
  value->format = 8;
  value->count  = size;
  value->size   = size;
  if (size)
    memcpy(value->data, data, size);
}

static void prop32(Atom type, const unsigned long * data, size_t count)
{
  struct Property * value = prop();
  const size_t size = count * sizeof(*data);
  CHECK(size <= sizeof(value->data));
  value->type   = type;
  value->format = 32;
  value->count  = count;
  value->size   = size;
  if (size)
    memcpy(value->data, data, size);
}

static void propNull(Atom type, int format, unsigned long count)
{
  struct Property * value = prop();
  value->type   = type;
  value->format = format;
  value->count  = count;
  value->null   = true;
}

static void selection(Window requestor, Atom target, Atom property)
{
  XEvent event = {};
  event.xselection.type      = SelectionNotify;
  event.xselection.display   = x11.display;
  event.xselection.requestor = requestor;
  event.xselection.selection = x11atoms.CLIPBOARD;
  event.xselection.target    = target;
  event.xselection.property  = property;
  CHECK(x11CBEventThread(&event));
}

static void propertyState(Window window, Atom atom, int state)
{
  XEvent event = {};
  event.xproperty.type    = PropertyNotify;
  event.xproperty.display = x11.display;
  event.xproperty.window  = window;
  event.xproperty.atom    = atom;
  event.xproperty.state   = state;
  CHECK(x11CBEventThread(&event));
}

static void property(Window window)
{
  propertyState(window, x11atoms.SEL_DATA, PropertyNewValue);
}

static void owner(Window owner)
{
  rec.owner = owner;
  XFixesSelectionNotifyEvent event =
  {
    .type      = x11.eventBase + XFixesSelectionNotify,
    .display   = x11.display,
    .window    = x11.window,
    .owner     = owner,
    .selection = x11atoms.CLIPBOARD,
  };
  CHECK(x11CBEventThread((const XEvent *)&event));
}

static void selectionClear(Window owner)
{
  rec.owner = owner;
  XEvent event = {};
  event.xselectionclear.type      = SelectionClear;
  event.xselectionclear.display   = x11.display;
  event.xselectionclear.window    = x11.window;
  event.xselectionclear.selection = x11atoms.CLIPBOARD;
  CHECK(x11CBEventThread(&event));
}

static void discover(Window ownerWindow,
    const unsigned long * targets, size_t count)
{
  const unsigned int no = rec.convertN;
  owner(ownerWindow);
  CHECK(rec.convertN == no + 1);
  const struct ConvertLog * convert = &rec.convert[no];
  CHECK(convert->selection == x11atoms.CLIPBOARD);
  CHECK(convert->target == x11atoms.TARGETS);
  CHECK(convert->property == x11atoms.TARGETS);
  prop32(XA_ATOM, targets, count);
  selection(convert->requestor, x11atoms.TARGETS, x11atoms.TARGETS);
}

static Window request(LG_ClipboardRequest id, LG_ClipboardData type)
{
  const unsigned int no = rec.convertN;
  x11CBRequest(id, type);
  CHECK(rec.convertN == no + 1);
  const struct ConvertLog * convert = &rec.convert[no];
  CHECK(convert->selection == x11atoms.CLIPBOARD);
  CHECK(convert->property == x11atoms.SEL_DATA);
  return convert->requestor;
}

static void selectionRequest(Atom target, Atom property)
{
  XEvent event = {};
  event.xselectionrequest.type      = SelectionRequest;
  event.xselectionrequest.display   = x11.display;
  event.xselectionrequest.owner     = x11.window;
  event.xselectionrequest.requestor = 900;
  event.xselectionrequest.selection = x11atoms.CLIPBOARD;
  event.xselectionrequest.target    = target;
  event.xselectionrequest.property  = property;
  CHECK(x11CBEventThread(&event));
}

static void start(void)
{
  memset(&x11, 0, sizeof(x11));
  memset(&x11atoms, 0, sizeof(x11atoms));
  memset(&rec, 0, sizeof(rec));
  rec.nextWindow     = 1000;
  rec.requestOK      = true;
  rec.fixesOK        = true;
  rec.beginResult    = LG_CLIPBOARD_RESULT_ACCEPTED;
  rec.chunkResult    = LG_CLIPBOARD_RESULT_ACCEPTED;
  rec.endResult      = LG_CLIPBOARD_RESULT_ACCEPTED;
  rec.fileSetOK      = true;
  rec.presentationOK = true;

  pthread_mutex_lock(&readRaceLock);
  readRacePause   = false;
  readRaceEntered = false;
  readRaceResume  = false;
  pthread_mutex_unlock(&readRaceLock);

  x11.display     = (Display *)(uintptr_t)1;
  x11.window      = 50;

  x11atoms.CLIPBOARD = A_CLIPBOARD;
  x11atoms.TARGETS   = A_TARGETS;
  x11atoms.SEL_DATA  = A_SEL_DATA;
  x11atoms.INCR      = A_INCR;

  CHECK(x11CBInit());
  CHECK(x11.eventBase == 80);
  CHECK(x11.errorBase == 81);
  CHECK(rec.selectN == 1);
}

static void finish(void)
{
  x11CBRelease();
  owner(None);
  CHECK(rec.owner == None);
  CHECK(rec.grabN == rec.ungrabN);
  for (unsigned int i = 0; i < rec.windowN; ++i)
    CHECK(rec.window[i].dead);
}

static void testTargets(void)
{
  start();
  const unsigned long targets[] = { A_PNG, 999, A_TEXT, A_PNG };
  discover(60, targets, ARRAY_LENGTH(targets));
  CHECK(rec.noticeN == 1);
  CHECK(rec.notice[0].count == 2);
  CHECK(rec.notice[0].type[0] == LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.notice[0].type[1] == LG_CLIPBOARD_DATA_PNG);
  CHECK(rec.destroyN == 1);
  CHECK(rec.freeN == 1);
  finish();
}

static void testFileImport(void)
{
  start();
  const unsigned long targets[] = { A_TEXT, A_URI };
  discover(601, targets, ARRAY_LENGTH(targets));
  CHECK(rec.convertN == 2);
  const struct ConvertLog * convert = &rec.convert[1];
  CHECK(convert->target == A_URI);
  CHECK(convert->property == x11atoms.SEL_DATA);
  CHECK(rec.noticeN == 0);

  const char payload[] =
    "file:///home/user/first.txt\r\nfile:///home/user/second.txt\r\n";
  prop8(A_URI, payload, sizeof(payload) - 1U);
  selection(convert->requestor, A_URI, x11atoms.SEL_DATA);
  CHECK(rec.fileSetN == 1);
  CHECK(strcmp(rec.fileMime, "text/uri-list") == 0);
  CHECK(rec.fileSize == sizeof(payload) - 1U);
  CHECK(memcmp(rec.fileData, payload, sizeof(payload) - 1U) == 0);
  CHECK(rec.noticeN == 0);
  finish();
}

static void testFileImportIncr(void)
{
  start();
  const unsigned long targets[] = { A_URI, A_TEXT, A_GNOME };
  discover(602, targets, ARRAY_LENGTH(targets));
  CHECK(rec.convertN == 2);
  const struct ConvertLog * convert = &rec.convert[1];
  CHECK(convert->target == A_GNOME);

  const unsigned long capacity = 128;
  prop32(x11atoms.INCR, &capacity, 1);
  selection(convert->requestor, A_GNOME, x11atoms.SEL_DATA);
  CHECK(rec.fileSetN == 0);

  const char first[] = "copy\nfile:///home/user/";
  const char second[] = "large%20file.txt\r\n";
  prop8(A_GNOME, first, sizeof(first) - 1U);
  property(convert->requestor);
  CHECK(rec.fileSetN == 0);
  prop8(A_GNOME, second, sizeof(second) - 1U);
  property(convert->requestor);
  propNull(A_GNOME, 8, 0);
  property(convert->requestor);

  CHECK(rec.fileSetN == 1);
  CHECK(strcmp(rec.fileMime,
      "x-special/gnome-copied-files") == 0);
  CHECK(rec.fileSize == sizeof(first) + sizeof(second) - 2U);
  CHECK(memcmp(rec.fileData,
      "copy\nfile:///home/user/large%20file.txt\r\n",
      rec.fileSize) == 0);
  CHECK(rec.noticeN == 0);
  finish();
}

static void testFileImportMate(void)
{
  start();
  const unsigned long targets[] = { A_TEXT, A_URI, A_MATE };
  discover(603, targets, ARRAY_LENGTH(targets));
  CHECK(rec.convertN == 2);
  const struct ConvertLog * convert = &rec.convert[1];
  CHECK(convert->target == A_MATE);
  CHECK(convert->property == x11atoms.SEL_DATA);

  const char payload[] =
    "copy\nfile:///home/user/first.txt\r\n"
    "file:///home/user/second.txt\r\n";
  prop8(A_MATE, payload, sizeof(payload) - 1U);
  selection(convert->requestor, A_MATE, x11atoms.SEL_DATA);
  CHECK(rec.fileSetN == 1);
  CHECK(strcmp(rec.fileMime, "x-special/mate-copied-files") == 0);
  CHECK(rec.fileSize == sizeof(payload) - 1U);
  CHECK(memcmp(rec.fileData, payload, sizeof(payload) - 1U) == 0);
  CHECK(rec.noticeN == 0);
  finish();
}

static void testFileSource(void)
{
  start();
  x11CBNotice(LG_CLIPBOARD_DATA_FILES);
  CHECK(rec.owner == x11.window);
  CHECK(rec.presentationAcquireN == 1);
  CHECK(rec.presentationReleaseN == 0);

  selectionRequest(x11atoms.TARGETS, 710);
  CHECK(rec.changeN == 1);
  CHECK(rec.change[0].format == 32);
  CHECK(rec.change[0].count == 5);
  const Atom * targets = (const Atom *)rec.change[0].data;
  CHECK(targets[0] == x11atoms.TARGETS);
  CHECK(targets[1] == A_URI);
  CHECK(targets[2] == A_GNOME);
  CHECK(targets[3] == A_MATE);
  CHECK(targets[4] == A_KDE);

  selectionRequest(A_URI, 711);
  CHECK(rec.presentationAcquireN == 2);
  CHECK(rec.structureSelectN == 1);
  CHECK(rec.changeN == 2);
  CHECK(rec.change[1].type == x11atoms.INCR);
  CHECK(rec.change[1].format == 32);
  CHECK(rec.change[1].count == 1);
  CHECK(rec.sendN == 2);

  propertyState(900, 711, PropertyDelete);
  CHECK(rec.changeN == 3);
  CHECK(rec.change[2].type == A_URI);
  CHECK(rec.change[2].format == 8);
  const char expected[] =
    "file:///run/user/1000/looking-glass/guest.txt\r\n";
  CHECK(rec.change[2].size == sizeof(expected) - 1U);
  CHECK(memcmp(rec.change[2].data,
      expected, sizeof(expected) - 1U) == 0);

  propertyState(900, 711, PropertyDelete);
  CHECK(rec.changeN == 4);
  CHECK(rec.change[3].type == A_URI);
  CHECK(rec.change[3].count == 0);
  CHECK(rec.presentationDeliveredN == 1);
  CHECK(rec.presentationDelivered[0] == 2001U);
  CHECK(rec.presentationReleaseN == 1);
  CHECK(rec.presentationReleased[0] == 2001U);

  selectionRequest(A_KDE, 712);
  CHECK(rec.presentationAcquireN == 3);
  CHECK(rec.structureSelectN == 2);
  CHECK(rec.changeN == 5);
  CHECK(rec.change[4].type == x11atoms.INCR);
  propertyState(900, 712, PropertyDelete);
  CHECK(rec.changeN == 6);
  CHECK(rec.change[5].type == A_KDE);
  CHECK(rec.change[5].size == 1);
  CHECK(rec.change[5].data[0] == '0');
  propertyState(900, 712, PropertyDelete);
  CHECK(rec.changeN == 7);
  CHECK(rec.change[6].type == A_KDE);
  CHECK(rec.change[6].count == 0);
  CHECK(rec.presentationDeliveredN == 1);
  CHECK(rec.presentationReleaseN == 2);
  CHECK(rec.presentationReleased[1] == 2001U);

  x11CBNotice(LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.presentationReleaseN == 3);
  CHECK(rec.presentationReleased[2] == 2001U);
  finish();
  CHECK(rec.presentationReleaseN == 3);
}

static void testFileSourceMate(void)
{
  start();
  x11CBNotice(LG_CLIPBOARD_DATA_FILES);
  CHECK(rec.presentationAcquireN == 1);
  CHECK(rec.presentationReleaseN == 0);

  selectionRequest(A_MATE, 713);
  CHECK(rec.presentationAcquireN == 2);
  CHECK(rec.structureSelectN == 1);
  CHECK(rec.changeN == 1);
  CHECK(rec.change[0].type == x11atoms.INCR);
  CHECK(rec.change[0].format == 32);
  CHECK(rec.change[0].count == 1);
  CHECK(rec.sendN == 1);

  propertyState(900, 713, PropertyDelete);
  CHECK(rec.changeN == 2);
  CHECK(rec.change[1].type == A_MATE);
  CHECK(rec.change[1].format == 8);
  const char expected[] =
    "copy\nfile:///run/user/1000/looking-glass/guest.txt\r\n";
  CHECK(rec.change[1].size == sizeof(expected) - 1U);
  CHECK(memcmp(rec.change[1].data,
      expected, sizeof(expected) - 1U) == 0);

  propertyState(900, 713, PropertyDelete);
  CHECK(rec.changeN == 3);
  CHECK(rec.change[2].type == A_MATE);
  CHECK(rec.change[2].count == 0);
  CHECK(rec.presentationDeliveredN == 1);
  CHECK(rec.presentationDelivered[0] == 2001U);
  CHECK(rec.presentationReleaseN == 1);
  CHECK(rec.presentationReleased[0] == 2001U);

  finish();
  CHECK(rec.presentationReleaseN == 2);
  CHECK(rec.presentationReleased[1] == 2001U);
}

static void testFileSourceActiveReplacement(void)
{
  start();
  x11CBNotice(LG_CLIPBOARD_DATA_FILES);
  selectionRequest(A_URI, 713);
  CHECK(rec.changeN == 1);
  CHECK(rec.change[0].type == x11atoms.INCR);
  CHECK(rec.presentationAcquireN == 2);
  CHECK(rec.presentationReleaseN == 0);

  x11CBNotice(LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.presentationReleaseN == 1);
  CHECK(rec.changeN == 1);

  selectionClear(901);
  CHECK(rec.presentationReleaseN == 1);
  CHECK(rec.changeN == 1);

  propertyState(900, 713, PropertyDelete);
  CHECK(rec.changeN == 2);
  CHECK(rec.change[1].type == A_URI);
  CHECK(rec.change[1].size > 0);
  CHECK(rec.presentationDeliveredN == 0);

  propertyState(900, 713, PropertyDelete);
  CHECK(rec.changeN == 3);
  CHECK(rec.change[2].type == A_URI);
  CHECK(rec.change[2].count == 0);
  CHECK(rec.presentationDeliveredN == 1);
  CHECK(rec.presentationDelivered[0] == 2001U);
  CHECK(rec.presentationReleaseN == 2);
  CHECK(rec.presentationReleased[1] == 2001U);
  finish();
  CHECK(rec.presentationReleaseN == 2);

  start();
  x11CBNotice(LG_CLIPBOARD_DATA_FILES);
  selectionRequest(A_URI, 714);
  CHECK(rec.changeN == 1);
  CHECK(rec.presentationAcquireN == 2);

  XEvent destroyed = {};
  destroyed.xdestroywindow.type    = DestroyNotify;
  destroyed.xdestroywindow.display = x11.display;
  destroyed.xdestroywindow.window  = 900;
  CHECK(x11CBEventThread(&destroyed));
  CHECK(rec.changeN == 1);
  CHECK(rec.presentationReleaseN == 1);
  CHECK(rec.presentationDeliveredN == 0);

  finish();
  CHECK(rec.presentationReleaseN == 2);
}

static void testNormal(void)
{
  start();
  const unsigned long targets[] = { A_TEXT };
  discover(61, targets, ARRAY_LENGTH(targets));
  const Window window = request(101, LG_CLIPBOARD_DATA_TEXT);
  const uint8_t data[] = "normal data";
  prop8(A_TEXT, data, sizeof(data) - 1);
  selection(window, A_TEXT, x11atoms.SEL_DATA);

  CHECK(rec.dataN == 1);
  CHECK(rec.data[0].request == 101);
  CHECK(rec.data[0].type == LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.data[0].size == sizeof(data) - 1);
  CHECK(memcmp(rec.data[0].data, data, sizeof(data) - 1) == 0);
  CHECK(rec.abortN == 0);
  CHECK(rec.destroyN == 2);
  CHECK(rec.freeN == 2);
  finish();
}

static void testIncr(void)
{
  start();
  const unsigned long targets[] = { A_TEXT };
  discover(62, targets, ARRAY_LENGTH(targets));
  const Window window = request(201, LG_CLIPBOARD_DATA_TEXT);
  const unsigned long capacity = 2;
  prop32(x11atoms.INCR, &capacity, 1);
  selection(window, A_TEXT, x11atoms.SEL_DATA);
  CHECK(rec.dataN == 0);
  CHECK(rec.abortN == 0);

  const uint8_t first[] = "abc";
  const uint8_t second[] = "defg";
  prop8(A_TEXT, first, sizeof(first) - 1);
  property(window);
  prop8(A_TEXT, second, sizeof(second) - 1);
  property(window);
  propNull(A_TEXT, 8, 0);
  property(window);

  CHECK(rec.dataN == 1);
  CHECK(rec.data[0].request == 201);
  CHECK(rec.data[0].type == LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.data[0].size == 7);
  CHECK(memcmp(rec.data[0].data, "abcdefg", 7) == 0);
  CHECK(rec.abortN == 0);
  CHECK(rec.destroyN == 2);
  CHECK(rec.freeN == 4);
  finish();
}

static void testIncrBlocked(void)
{
  start();
  const unsigned long targets[] = { A_TEXT };
  discover(620, targets, ARRAY_LENGTH(targets));
  const Window window = request(211, LG_CLIPBOARD_DATA_TEXT);
  const unsigned long capacity = 9;
  rec.beginResult = LG_CLIPBOARD_RESULT_BLOCKED;
  prop32(x11atoms.INCR, &capacity, 1);
  selection(window, A_TEXT, x11atoms.SEL_DATA);
  CHECK(rec.streamBeginN == 1);
  CHECK(rec.dataN == 0);

  const uint8_t first[] = "abc";
  prop8(A_TEXT, first, sizeof(first) - 1);
  property(window);
  CHECK(rec.streamChunkN == 0);

  rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED;
  rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
  x11CBRequestReady(211);
  CHECK(rec.streamBeginN == 2);
  CHECK(rec.streamSizeHint == capacity);
  CHECK(rec.streamChunkN == 1);
  CHECK(rec.data[0].size == 0);

  const uint8_t second[] = "defghi";
  prop8(A_TEXT, second, sizeof(second) - 1);
  property(window);
  CHECK(rec.streamChunkN == 1);

  rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
  x11CBRequestReady(211);
  CHECK(rec.streamChunkN == 3);
  CHECK(rec.data[0].size == 9);
  CHECK(memcmp(rec.data[0].data, "abcdefghi", 9) == 0);

  rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED;
  propNull(A_TEXT, 8, 0);
  property(window);
  CHECK(rec.streamEndN == 1);
  CHECK(rec.dataN == 0);
  rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
  x11CBRequestReady(211);
  CHECK(rec.streamEndN == 2);
  CHECK(rec.dataN == 1);
  CHECK(rec.abortN == 0);
  finish();
}

static void testIncrReadyRace(void)
{
  start();
  const unsigned long targets[] = { A_TEXT };
  discover(622, targets, ARRAY_LENGTH(targets));
  const Window window = request(213, LG_CLIPBOARD_DATA_TEXT);
  const unsigned long capacity = 4;
  prop32(x11atoms.INCR, &capacity, 1);
  selection(window, A_TEXT, x11atoms.SEL_DATA);

  const uint8_t data[] = "race";
  rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
  rec.readyDuringChunk = true;
  prop8(A_TEXT, data, sizeof(data) - 1);
  property(window);
  CHECK(rec.streamChunkN == 2);
  CHECK(rec.data[0].size == sizeof(data) - 1);

  rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED;
  rec.readyDuringEnd = true;
  propNull(A_TEXT, 8, 0);
  property(window);
  CHECK(rec.streamEndN == 2);
  CHECK(rec.dataN == 1);
  CHECK(rec.abortN == 0);
  finish();
}

static void * requestReadyThread(void * opaque)
{
  x11CBRequestReady(*(const LG_ClipboardRequest *)opaque);
  return NULL;
}

static void testIncrCancelRace(void)
{
  start();
  const unsigned long targets[] = { A_TEXT };
  discover(623, targets, ARRAY_LENGTH(targets));
  const LG_ClipboardRequest id = 214;
  const Window window = request(id, LG_CLIPBOARD_DATA_TEXT);
  const unsigned long capacity = 6;
  prop32(x11atoms.INCR, &capacity, 1);
  selection(window, A_TEXT, x11atoms.SEL_DATA);

  const uint8_t data[] = "cancel";
  rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
  prop8(A_TEXT, data, sizeof(data) - 1);
  property(window);
  CHECK(rec.streamChunkN == 1);

  rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
  pthread_mutex_lock(&readRaceLock);
  readRacePause = true;
  pthread_mutex_unlock(&readRaceLock);

  pthread_t readyThread;
  CHECK(pthread_create(&readyThread, NULL, requestReadyThread,
      (void *)&id) == 0);

  pthread_mutex_lock(&readRaceLock);
  while (!readRaceEntered)
    pthread_cond_wait(&readRaceCond, &readRaceLock);
  pthread_mutex_unlock(&readRaceLock);

  /* The callback is outside x11cb.lock and still consuming its chunk. */
  x11CBRequestCancel(id, LG_CLIPBOARD_CANCEL_REPLACED);

  pthread_mutex_lock(&readRaceLock);
  readRaceResume = true;
  pthread_cond_signal(&readRaceCond);
  pthread_mutex_unlock(&readRaceLock);

  CHECK(pthread_join(readyThread, NULL) == 0);
  CHECK(rec.streamChunkN == 2);
  CHECK(rec.data[0].size == sizeof(data) - 1);
  CHECK(memcmp(rec.data[0].data, data, sizeof(data) - 1) == 0);
  CHECK(rec.abortN == 0);
  finish();
}

static void testIncrLargeProperty(void)
{
  start();
  const unsigned long targets[] = { A_TEXT };
  discover(621, targets, ARRAY_LENGTH(targets));
  const Window window = request(212, LG_CLIPBOARD_DATA_TEXT);
  const unsigned long capacity = 70U * 1024U;
  prop32(x11atoms.INCR, &capacity, 1);
  selection(window, A_TEXT, x11atoms.SEL_DATA);

  const size_t size = 70U * 1024U;
  uint8_t * data = malloc(size);
  CHECK(data);
  for (size_t i = 0; i < size; ++i)
    data[i] = (uint8_t)i;
  prop8(A_TEXT, data, size);
  property(window);
  propNull(A_TEXT, 8, 0);
  property(window);

  CHECK(rec.streamChunkN == 2);
  CHECK(rec.streamMaxChunk == KVMFR_CLIPBOARD_REPRESENTATION_BYTES);
  CHECK(rec.dataN == 1);
  CHECK(rec.data[0].size == size);
  CHECK(memcmp(rec.data[0].data, data, size) == 0);
  free(data);
  finish();
}

static void testMalformed(void)
{
  start();
  const unsigned long targets[] = { A_TEXT };
  discover(63, targets, ARRAY_LENGTH(targets));

  Window window = request(301, LG_CLIPBOARD_DATA_TEXT);
  const unsigned long bad = 1;
  prop32(A_TEXT, &bad, 1);
  selection(window, A_TEXT, x11atoms.SEL_DATA);
  CHECK(rec.abortN == 1);
  CHECK(rec.abort[0] == 301);

  window = request(302, LG_CLIPBOARD_DATA_TEXT);
  const unsigned long capacity = 8;
  prop32(x11atoms.INCR, &capacity, 1);
  selection(window, A_TEXT, x11atoms.SEL_DATA);
  prop32(A_TEXT, &bad, 1);
  property(window);
  CHECK(rec.abortN == 2);
  CHECK(rec.abort[1] == 302);

  window = request(303, LG_CLIPBOARD_DATA_TEXT);
  propNull(A_TEXT, 8, 3);
  selection(window, A_TEXT, x11atoms.SEL_DATA);
  CHECK(rec.abortN == 3);
  CHECK(rec.abort[2] == 303);
  CHECK(rec.dataN == 0);
  finish();
}

static void testReplace(void)
{
  start();
  const unsigned long targets[] = { A_TEXT };
  discover(64, targets, ARRAY_LENGTH(targets));
  const Window first = request(401, LG_CLIPBOARD_DATA_TEXT);

  const unsigned int convertN = rec.convertN;
  owner(65);
  CHECK(rec.convertN == convertN + 1);
  CHECK(rec.abortN == 1);
  CHECK(rec.abort[0] == 401);
  CHECK(rec.window[1].window == first);
  CHECK(rec.window[1].dead);

  const Window second = request(402, LG_CLIPBOARD_DATA_TEXT);
  owner(None);
  CHECK(rec.abortN == 2);
  CHECK(rec.abort[1] == 402);
  CHECK(rec.releaseN == 1);
  bool dead = false;
  for (unsigned int i = 0; i < rec.windowN; ++i)
    if (rec.window[i].window == second)
      dead = rec.window[i].dead;
  CHECK(dead);

  x11CBRequest(403, LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.abortN == 3);
  CHECK(rec.abort[2] == 403);
  finish();
}

static void testSource(void)
{
  start();
  x11CBNotice(LG_CLIPBOARD_DATA_PNG);
  CHECK(rec.owner == x11.window);

  selectionRequest(x11atoms.TARGETS, 700);
  CHECK(rec.changeN == 1);
  CHECK(rec.change[0].window == 900);
  CHECK(rec.change[0].property == 700);
  CHECK(rec.change[0].type == XA_ATOM);
  CHECK(rec.change[0].format == 32);
  CHECK(rec.change[0].count == 2);
  const Atom * values = (const Atom *)rec.change[0].data;
  CHECK(values[0] == x11atoms.TARGETS);
  CHECK(values[1] == A_PNG);
  CHECK(rec.sendN == 1);
  CHECK(rec.send[0].xselection.property == 700);

  selectionRequest(A_PNG, 701);
  CHECK(rec.requestN == 1);
  CHECK(rec.requestType == LG_CLIPBOARD_DATA_PNG);
  CHECK(rec.requestStream);
  CHECK(rec.sendN == 1);
  const uint8_t data[] = { 1, 2, 3, 4 };
  CHECK(rec.requestStream->begin(rec.requestOpaque,
      LG_CLIPBOARD_DATA_PNG, LG_CLIPBOARD_SIZE_UNKNOWN) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.changeN == 2);
  CHECK(rec.change[1].property == 701);
  CHECK(rec.change[1].type == x11atoms.INCR);
  CHECK(rec.change[1].format == 32);
  CHECK(rec.change[1].count == 1);
  CHECK(rec.sendN == 2);
  CHECK(rec.send[1].xselection.property == 701);

  CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
      data, ARRAY_LENGTH(data)) == LG_CLIPBOARD_RESULT_BLOCKED);
  propertyState(900, 701, PropertyDelete);
  CHECK(rec.requestReadyN == 1);
  CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
      data, ARRAY_LENGTH(data)) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.changeN == 3);
  CHECK(rec.change[2].property == 701);
  CHECK(rec.change[2].type == A_PNG);
  CHECK(rec.change[2].format == 8);
  CHECK(rec.change[2].count == 4);
  CHECK(memcmp(rec.change[2].data, data, sizeof(data)) == 0);
  CHECK(rec.requestStream->end(rec.requestOpaque, ARRAY_LENGTH(data)) ==
      LG_CLIPBOARD_RESULT_BLOCKED);
  propertyState(900, 701, PropertyDelete);
  CHECK(rec.requestReadyN == 2);
  CHECK(rec.requestStream->end(rec.requestOpaque, ARRAY_LENGTH(data)) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.changeN == 4);
  CHECK(rec.change[3].property == 701);
  CHECK(rec.change[3].type == A_PNG);
  CHECK(rec.change[3].format == 8);
  CHECK(rec.change[3].count == 0);

  rec.requestCancelSync = true;
  selectionRequest(A_PNG, 704);
  CHECK(rec.requestN == 2);
  CHECK(rec.sendN == 3);
  CHECK(rec.send[2].xselection.property == None);
  rec.requestCancelSync = false;

  selectionRequest(A_TEXT, 702);
  CHECK(rec.requestN == 2);
  CHECK(rec.sendN == 4);
  CHECK(rec.send[3].xselection.property == None);

  rec.requestOK = false;
  selectionRequest(A_PNG, 703);
  CHECK(rec.requestN == 3);
  CHECK(rec.sendN == 5);
  CHECK(rec.send[4].xselection.property == None);

  x11CBRelease();
  CHECK(rec.owner == None);
  finish();
}

static void testSourceBeginDuringRequest(void)
{
  start();
  x11CBNotice(LG_CLIPBOARD_DATA_PNG);
  rec.requestBeginSync = true;
  selectionRequest(A_PNG, 705);
  CHECK(rec.requestN == 1);
  CHECK(rec.requestBeginResult == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.requestOpaque);
  CHECK(rec.changeN == 1);
  CHECK(rec.change[0].type == x11atoms.INCR);
  CHECK(rec.sendN == 1);

  propertyState(900, 705, PropertyDelete);
  CHECK(rec.requestStream->end(rec.requestOpaque, 0) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.changeN == 2);
  CHECK(rec.change[1].count == 0);
  finish();
}

static void testTeardown(void)
{
  start();
  const unsigned int convertN = rec.convertN;
  owner(66);
  CHECK(rec.convertN == convertN + 1);
  const Window targets = rec.convert[convertN].requestor;
  const Window read = request(501, LG_CLIPBOARD_DATA_TEXT);
  x11CBNotice(LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.abortN == 1);
  CHECK(rec.abort[0] == 501);
  CHECK(rec.owner == x11.window);
  CHECK(rec.destroyN == 2);

  bool targetsDead = false;
  bool readDead    = false;
  for (unsigned int i = 0; i < rec.windowN; ++i)
  {
    if (rec.window[i].window == targets)
      targetsDead = rec.window[i].dead;
    if (rec.window[i].window == read)
      readDead = rec.window[i].dead;
  }
  CHECK(targetsDead);
  CHECK(readDead);

  x11CBRelease();
  CHECK(rec.owner == None);
  finish();
}

struct Test
{
  const char * name;
  void (*run)(void);
};

static const struct Test tests[] =
{
  { "targets"                   , testTargets                     },
  { "file-import"               , testFileImport                  },
  { "file-incr"                 , testFileImportIncr              },
  { "file-mate-import"          , testFileImportMate              },
  { "file-source"               , testFileSource                  },
  { "file-mate-source"          , testFileSourceMate              },
  { "file-source-active-replace", testFileSourceActiveReplacement },
  { "normal"                    , testNormal                      },
  { "incr"                      , testIncr                        },
  { "incr-block"                , testIncrBlocked                 },
  { "incr-ready"                , testIncrReadyRace               },
  { "incr-cancel"               , testIncrCancelRace              },
  { "incr-large"                , testIncrLargeProperty           },
  { "malformed"                 , testMalformed                   },
  { "replace"                   , testReplace                     },
  { "source"                    , testSource                      },
  { "source-early"              , testSourceBeginDuringRequest    },
  { "teardown"                  , testTeardown                    },
};

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

  if (argc == 2)
  {
    for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i)
      if (strcmp(argv[1], tests[i].name) == 0)
      {
        tests[i].run();
        return 0;
      }

    fprintf(stderr, "unknown test: %s\n", argv[1]);
    return EXIT_FAILURE;
  }

  if (argc != 1)
    return EXIT_FAILURE;

  for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i)
    tests[i].run();
  return 0;
}
