/**
 * 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 "main.h"
#include "render_queue.h"
#include "test.h"

#include "common/debug.h"

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

#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0]))

struct AppState g_state;

struct Fake
{
  LG_Renderer                 renderer;
  struct LG_DisplayServerOps  ds;

  bool                 prepResult;
  unsigned int         prepCount;
  RenderQueueSource    prepSource;
  uint64_t             prepGeneration;
  uint64_t             prepSerial;

  unsigned int         appliedCount;
  RenderQueueSource    appliedSource;
  uint64_t             appliedGeneration;
  uint64_t             appliedSerial;
  bool                 appliedSwSurface;

  unsigned int         invalidates;
  unsigned int         showCount;
  bool                 show;
  unsigned int         configCount;
  int                  configWidth;
  int                  configHeight;
  unsigned int         fillCount;
  int                  fillX;
  int                  fillY;
  int                  fillWidth;
  int                  fillHeight;
  uint32_t             fillColor;
  unsigned int         bitmapCount;
  int                  bitmapX;
  int                  bitmapY;
  int                  bitmapWidth;
  int                  bitmapHeight;
  int                  bitmapStride;
  bool                 bitmapTopDown;
  uint8_t              bitmap[64];
  size_t               bitmapSize;

  unsigned int          shapeCount;
  LG_RendererCursor     shapeType;
  int                   shapeWidth;
  int                   shapeHeight;
  int                   shapePitch;
  uint8_t               shape[64];
  size_t                shapeSize;
  unsigned int          cursorCount;
  bool                  cursorVisible;
  int                   cursorX;
  int                   cursorY;
  int                   cursorHX;
  int                   cursorHY;
  unsigned int          colorCount;
  LGColorTransformFlags colorFlags;
  float                 colorScalar;
  float                 colorMatrix;
  float                 colorLut;
  unsigned int          whiteCount;
  uint32_t              whiteLevel;

  unsigned int         hdrCount;
  LG_RendererFormat    hdr[8];
};

static struct Fake f;

void app_invalidateWindow(bool full)
{
  (void)full;
  ++f.invalidates;
}

static bool prep(void * opaque, RenderQueueSource source,
    uint64_t generation, uint64_t serial)
{
  struct Fake * fake   = opaque;
  fake->prepCount++;
  fake->prepSource     = source;
  fake->prepGeneration = generation;
  fake->prepSerial     = serial;
  return fake->prepResult;
}

static void applied(void * opaque, RenderQueueSource source,
    uint64_t generation, uint64_t serial, bool swSurface)
{
  struct Fake * fake      = opaque;
  fake->appliedCount++;
  fake->appliedSource     = source;
  fake->appliedGeneration = generation;
  fake->appliedSerial     = serial;
  fake->appliedSwSurface  = swSurface;
}

static void swShow(LG_Renderer * renderer, bool show)
{
  (void)renderer;
  ++f.showCount;
  f.show = show;
}

static void swConfig(LG_Renderer * renderer, int width, int height)
{
  (void)renderer;
  ++f.configCount;
  f.configWidth  = width;
  f.configHeight = height;
}

static void swFill(LG_Renderer * renderer, int x, int y,
    int width, int height, uint32_t color)
{
  (void)renderer;
  ++f.fillCount;
  f.fillX      = x;
  f.fillY      = y;
  f.fillWidth  = width;
  f.fillHeight = height;
  f.fillColor  = color;
}

static void swBitmap(LG_Renderer * renderer, int x, int y,
    int width, int height, int stride, uint8_t * data, bool topDown)
{
  (void)renderer;
  ++f.bitmapCount;
  f.bitmapX       = x;
  f.bitmapY       = y;
  f.bitmapWidth   = width;
  f.bitmapHeight  = height;
  f.bitmapStride  = stride;
  f.bitmapTopDown = topDown;
  f.bitmapSize    = (size_t)height * (size_t)stride;
  CHECK(f.bitmapSize <= sizeof(f.bitmap));
  memcpy(f.bitmap, data, f.bitmapSize);
}

static bool mouseShape(LG_Renderer * renderer, LG_RendererCursor type,
    int width, int height, int pitch, const uint8_t * data)
{
  (void)renderer;
  ++f.shapeCount;
  f.shapeType   = type;
  f.shapeWidth  = width;
  f.shapeHeight = height;
  f.shapePitch  = pitch;
  f.shapeSize   = (size_t)height * (size_t)pitch;
  CHECK(f.shapeSize <= sizeof(f.shape));
  memcpy(f.shape, data, f.shapeSize);
  return true;
}

static bool mouseEvent(LG_Renderer * renderer, bool visible,
    int x, int y, int hx, int hy)
{
  (void)renderer;
  ++f.cursorCount;
  f.cursorVisible = visible;
  f.cursorX       = x;
  f.cursorY       = y;
  f.cursorHX      = hx;
  f.cursorHY      = hy;
  return true;
}

static void mouseColor(LG_Renderer * renderer,
    const LGColorTransform * color)
{
  (void)renderer;
  ++f.colorCount;
  f.colorFlags  = color->flags;
  f.colorScalar = color->scalar;
  f.colorMatrix = color->matrix[0][0];
  f.colorLut    = color->lut[0][0];
}

static void mouseWhite(LG_Renderer * renderer, uint32_t level)
{
  (void)renderer;
  ++f.whiteCount;
  f.whiteLevel = level;
}

static bool setHDR(const void * data)
{
  CHECK(f.hdrCount < ARRAY_LENGTH(f.hdr));
  f.hdr[f.hdrCount++] = *(const LG_RendererFormat *)data;
  return true;
}

static void start(void)
{
  memset(&f, 0, sizeof(f));
  memset(&g_state, 0, sizeof(g_state));

  f.prepResult = true;
  f.renderer.ops.onMouseShape          = mouseShape;
  f.renderer.ops.onMouseEvent          = mouseEvent;
  f.renderer.ops.onMouseColorTransform = mouseColor;
  f.renderer.ops.onMouseWhiteLevel     = mouseWhite;
  f.renderer.ops.swSurfaceConfigure    = swConfig;
  f.renderer.ops.swSurfaceDrawFill     = swFill;
  f.renderer.ops.swSurfaceDrawBitmap   = swBitmap;
  f.renderer.ops.swSurfaceShow         = swShow;
  f.ds.setHDRImageDesc                 = setHDR;
  g_state.lgr                          = &f.renderer;
  g_state.ds                           = &f.ds;

  renderQueue_init();
  renderQueue_setSourceFns(prep, NULL, applied, &f);
}

static void stop(void)
{
  renderQueue_setSourceFns(NULL, NULL, NULL, NULL);
  renderQueue_free();
}

static void testGeneration(void)
{
  start();

  const uint64_t generation =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  CHECK(generation != 0);

  renderQueue_sourceSwSurfaceDrawFill(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, 1, 2, 3, 4, 5);
  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        generation, true, NULL) != 0);
  renderQueue_sourceInvalidate(RENDER_QUEUE_SOURCE_PRIMARY, generation);
  renderQueue_process();
  renderQueue_presented();

  CHECK(f.fillCount == 0);
  CHECK(f.prepCount == 0);
  CHECK(f.appliedCount == 0);
  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        generation, false, NULL) == 0);

  const uint64_t current =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  CHECK(current != 0);
  CHECK(current != generation);
  renderQueue_sourceInvalidate(RENDER_QUEUE_SOURCE_PRIMARY, generation);
  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        current, false, NULL) != 0);
  renderQueue_process();
  CHECK(f.prepCount == 1);
  CHECK(f.prepGeneration == current);

  stop();
}

static void testLatest(void)
{
  start();

  const uint64_t primary =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  const uint64_t fallback =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_FALLBACK);
  CHECK(renderQueue_sourceSwSurfaceConfigure(
        RENDER_QUEUE_SOURCE_FALLBACK, fallback, 2, 1));
  atomic_uint_least64_t published;
  atomic_init(&published, 0);

  const uint64_t old = renderQueue_sourceTransition(
      RENDER_QUEUE_SOURCE_PRIMARY, primary, false, &published);
  CHECK(old != 0);
  CHECK(atomic_load(&published) == old);
  const uint64_t latest = renderQueue_sourceTransition(
      RENDER_QUEUE_SOURCE_FALLBACK, fallback, true, &published);
  CHECK(latest > old);
  CHECK(atomic_load(&published) == latest);

  renderQueue_process();
  CHECK(f.prepCount == 1);
  CHECK(f.prepSource == RENDER_QUEUE_SOURCE_FALLBACK);
  CHECK(f.prepGeneration == fallback);
  CHECK(f.prepSerial == latest);
  CHECK(f.showCount == 1);
  CHECK(f.show);

  renderQueue_presented();
  CHECK(f.appliedCount == 1);
  CHECK(f.appliedSource == RENDER_QUEUE_SOURCE_FALLBACK);
  CHECK(f.appliedGeneration == fallback);
  CHECK(f.appliedSerial == latest);
  CHECK(f.appliedSwSurface);

  stop();
}

static void testPrepare(void)
{
  start();

  const uint64_t generation =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  CHECK(renderQueue_sourceSwSurfaceConfigure(
        RENDER_QUEUE_SOURCE_PRIMARY, generation, 4, 4));
  f.prepResult = false;
  CHECK(renderQueue_sourceSwSurfaceConfigureTransition(
        RENDER_QUEUE_SOURCE_PRIMARY, generation, 4, 4, NULL) != 0);
  renderQueue_process();
  renderQueue_presented();

  CHECK(f.prepCount == 1);
  CHECK(f.configCount == 0);
  CHECK(f.fillCount == 0);
  CHECK(f.showCount == 0);
  CHECK(f.appliedCount == 0);

  f.prepResult = true;
  CHECK(renderQueue_sourceSwSurfaceConfigure(
        RENDER_QUEUE_SOURCE_PRIMARY, generation, 3, 2));
  const uint64_t serial = renderQueue_sourceSwSurfaceConfigureTransition(
      RENDER_QUEUE_SOURCE_PRIMARY, generation, 3, 2, NULL);
  CHECK(serial != 0);
  renderQueue_process();
  CHECK(f.configCount == 1);
  CHECK(f.configWidth == 3);
  CHECK(f.configHeight == 2);
  CHECK(f.fillCount == 0);
  CHECK(f.bitmapCount == 1);
  CHECK(f.bitmapX == 0);
  CHECK(f.bitmapY == 0);
  CHECK(f.bitmapWidth == 3);
  CHECK(f.bitmapHeight == 2);
  CHECK(f.bitmapStride == 12);
  CHECK(f.bitmapTopDown);
  CHECK(f.showCount == 1);
  CHECK(f.show);

  renderQueue_presented();
  CHECK(f.appliedCount == 1);
  CHECK(f.appliedSerial == serial);
  CHECK(f.appliedSwSurface);

  stop();
}

static void testPresented(void)
{
  start();

  const uint64_t primary =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  const uint64_t fallback =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_FALLBACK);
  const uint64_t first = renderQueue_sourceTransition(
      RENDER_QUEUE_SOURCE_PRIMARY, primary, false, NULL);
  CHECK(first != 0);
  renderQueue_process();
  CHECK(f.appliedCount == 0);

  const uint64_t latest = renderQueue_sourceTransition(
      RENDER_QUEUE_SOURCE_FALLBACK, fallback, false, NULL);
  CHECK(latest > first);
  renderQueue_process();
  CHECK(f.appliedCount == 0);

  renderQueue_presented();
  CHECK(f.appliedCount == 1);
  CHECK(f.appliedSource == RENDER_QUEUE_SOURCE_FALLBACK);
  CHECK(f.appliedGeneration == fallback);
  CHECK(f.appliedSerial == latest);
  renderQueue_presented();
  CHECK(f.appliedCount == 1);

  stop();
}

static void testPayload(void)
{
  start();

  const uint64_t generation =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  CHECK(renderQueue_sourceSwSurfaceConfigure(
        RENDER_QUEUE_SOURCE_PRIMARY, generation, 2, 2));
  uint8_t bitmap[] =
  {
     1,  2,  3,  4,  5,  6,  7,  8,
     9, 10, 11, 12, 13, 14, 15, 16,
  };
  const uint8_t bitmapExpected[] =
  {
     1,  2,  3,  4,  5,  6,  7,  8,
     9, 10, 11, 12, 13, 14, 15, 16,
  };
  uint8_t shape[] =
  {
    11, 12, 13, 14, 15, 16, 17, 18,
    19, 20, 21, 22, 23, 24, 25, 26,
  };
  const uint8_t shapeExpected[] =
  {
    11, 12, 13, 14, 15, 16, 17, 18,
    19, 20, 21, 22, 23, 24, 25, 26,
  };
  LGColorTransform color =
  {
    .flags        = LG_COLOR_TRANSFORM_MATRIX | LG_COLOR_TRANSFORM_LUT,
    .matrix[0][0] = 2.0f,
    .scalar       = 3.0f,
    .lut[0][0]    = 4.0f,
  };

  renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, 0, 0, 2, 2, 8, bitmap, true);
  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      LG_CURSOR_COLOR, 2, 2, 8, shape);
  renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, &color);
  renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      true, 20, 21, 1, 2);
  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        generation, true, NULL) != 0);

  memset(bitmap, 0, sizeof(bitmap));
  memset(shape, 0, sizeof(shape));
  color.flags        = 0;
  color.matrix[0][0] = 0.0f;
  color.scalar       = 0.0f;
  color.lut[0][0]    = 0.0f;
  renderQueue_process();

  CHECK(f.bitmapCount == 1);
  CHECK(f.bitmapX == 0);
  CHECK(f.bitmapY == 0);
  CHECK(f.bitmapWidth == 2);
  CHECK(f.bitmapHeight == 2);
  CHECK(f.bitmapStride == 8);
  CHECK(f.bitmapTopDown);
  CHECK(f.bitmapSize == sizeof(bitmapExpected));
  CHECK(memcmp(f.bitmap, bitmapExpected, sizeof(bitmapExpected)) == 0);
  CHECK(f.shapeCount == 1);
  CHECK(f.shapeType == LG_CURSOR_COLOR);
  CHECK(f.shapeWidth == 2);
  CHECK(f.shapeHeight == 2);
  CHECK(f.shapePitch == 8);
  CHECK(f.shapeSize == sizeof(shapeExpected));
  CHECK(memcmp(f.shape, shapeExpected, sizeof(shapeExpected)) == 0);
  CHECK(f.colorCount == 1);
  CHECK(f.colorFlags ==
      (LG_COLOR_TRANSFORM_MATRIX | LG_COLOR_TRANSFORM_LUT));
  CHECK(f.colorMatrix == 2.0f);
  CHECK(f.colorScalar == 3.0f);
  CHECK(f.colorLut == 4.0f);
  CHECK(f.cursorCount == 1);
  CHECK(f.cursorVisible);
  CHECK(f.cursorX == 20);
  CHECK(f.cursorY == 21);
  CHECK(f.cursorHX == 1);
  CHECK(f.cursorHY == 2);

  stop();
}

static void testValidation(void)
{
  start();

  const uint64_t generation =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  uint8_t data[16] = { 0 };

  renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, 0, 0, 0, 1, 4, data, false);
  renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, 0, 0, 1, 0, 4, data, false);
  renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, 0, 0, 1, 1, 0, data, false);
  renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, 0, 0, 1, 1, 4, NULL, false);
  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      LG_CURSOR_COLOR, 0, 1, 4, data);
  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      LG_CURSOR_COLOR, 1, 0, 4, data);
  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      LG_CURSOR_COLOR, 1, 1, 0, data);
  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      LG_CURSOR_COLOR, 1, 1, 4, NULL);
  renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      true, 1, 2, 3, 4);
  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        generation, false, NULL) != 0);
  renderQueue_process();

  CHECK(f.bitmapCount == 0);
  CHECK(f.shapeCount == 0);
  CHECK(f.cursorCount == 1);
  CHECK(!f.cursorVisible);

  stop();
}

static void testCache(void)
{
  start();

  const uint64_t generation =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  const uint8_t shape[] = { 1, 2, 3, 4 };
  LGColorTransform color =
  {
    .flags        = LG_COLOR_TRANSFORM_MATRIX,
    .matrix[0][0] = 1.5f,
    .scalar       = 2.5f,
  };
  LG_RendererFormat format =
  {
    .type          = FRAME_TYPE_RGBA,
    .hdr           = true,
    .hdrPQ         = true,
    .hdrMetadata   = true,
    .screenWidth   = 1920,
    .screenHeight  = 1080,
    .sdrWhiteLevel = 250,
  };

  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      LG_CURSOR_COLOR, 1, 1, 4, shape);
  renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      true, 30, 31, 2, 3);
  renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, &color);
  renderQueue_sourceCursorWhiteLevel(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, 250);
  renderQueue_sourceSurfaceFormat(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      format, true);
  renderQueue_process();
  CHECK(f.shapeCount == 0);
  CHECK(f.cursorCount == 0);
  CHECK(f.colorCount == 0);
  CHECK(f.whiteCount == 0);
  CHECK(f.hdrCount == 0);

  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        generation, false, NULL) != 0);
  renderQueue_process();
  CHECK(f.shapeCount == 1);
  CHECK(f.cursorCount == 1);
  CHECK(f.cursorVisible);
  CHECK(f.colorCount == 1);
  CHECK(f.colorScalar == 2.5f);
  CHECK(f.whiteCount == 1);
  CHECK(f.whiteLevel == 250);
  CHECK(f.hdrCount == 1);
  CHECK(f.hdr[0].type == FRAME_TYPE_RGBA);
  CHECK(f.hdr[0].hdr);
  CHECK(f.hdr[0].hdrPQ);
  CHECK(f.hdr[0].screenWidth == 1920);

  renderQueue_sourceInvalidate(RENDER_QUEUE_SOURCE_PRIMARY, generation);
  const uint64_t current =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  CHECK(current != generation);
  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        current, false, NULL) != 0);
  renderQueue_process();
  CHECK(f.shapeCount == 2);
  CHECK(f.cursorCount == 2);
  CHECK(f.cursorVisible);
  CHECK(f.cursorX == 30);
  CHECK(f.cursorY == 31);
  CHECK(f.cursorHX == 2);
  CHECK(f.cursorHY == 3);
  CHECK(f.colorCount == 2);
  CHECK(f.colorScalar == 2.5f);
  CHECK(f.whiteCount == 2);
  CHECK(f.whiteLevel == 250);
  CHECK(f.hdrCount == 2);
  CHECK(f.hdr[1].type == 0);
  CHECK(!f.hdr[1].hdr);

  renderQueue_sourceClearCursor(RENDER_QUEUE_SOURCE_PRIMARY);
  const uint64_t clear =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        clear, false, NULL) != 0);
  renderQueue_process();
  CHECK(f.shapeCount == 2);
  CHECK(f.cursorCount == 4);
  CHECK(!f.cursorVisible);
  CHECK(f.colorCount == 4);
  CHECK(f.colorFlags == 0);
  CHECK(f.colorScalar == 0.0f);
  CHECK(f.whiteCount == 4);
  CHECK(f.whiteLevel == LG_SDR_WHITE_LEVEL_DEFAULT);

  stop();
}

static void testCursorReplacement(void)
{
  start();

  const uint64_t generation =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  const uint8_t shape[] = { 1, 2, 3, 4 };
  LGColorTransform color =
  {
    .flags  = LG_COLOR_TRANSFORM_LUT,
    .scalar = 2.0f,
  };
  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        generation, false, NULL) != 0);
  renderQueue_process();
  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      LG_CURSOR_COLOR, 1, 1, 4, shape);
  renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, &color);
  renderQueue_sourceCursorWhiteLevel(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, 250);
  renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      true, 5, 6, 1, 2);
  renderQueue_process();
  CHECK(f.shapeCount == 1);
  CHECK(f.cursorVisible);
  f.shapeCount  = 0;
  f.colorCount  = 0;
  f.whiteCount  = 0;
  f.cursorCount = 0;
  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      LG_CURSOR_COLOR, 1, 1, 4, shape);
  renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, &color);
  renderQueue_sourceCursorWhiteLevel(RENDER_QUEUE_SOURCE_PRIMARY,
      generation, 250);
  renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      true, 10, 11, 1, 2);

  renderQueue_sourceClearCursor(RENDER_QUEUE_SOURCE_PRIMARY);
  CHECK(f.shapeCount == 0);
  CHECK(f.colorCount == 0);
  CHECK(f.whiteCount == 0);
  CHECK(f.cursorCount == 0);
  renderQueue_process();
  CHECK(f.shapeCount == 0);
  CHECK(f.colorCount == 1);
  CHECK(f.colorFlags == 0);
  CHECK(f.whiteCount == 1);
  CHECK(f.whiteLevel == LG_SDR_WHITE_LEVEL_DEFAULT);
  CHECK(f.cursorCount == 1);
  CHECK(!f.cursorVisible);
  f.shapeCount  = 0;
  f.colorCount  = 0;
  f.whiteCount  = 0;
  f.cursorCount = 0;
  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      LG_CURSOR_COLOR, 1, 1, 4, shape);
  renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation,
      true, 12, 13, 1, 2);
  renderQueue_process();
  CHECK(f.shapeCount == 1);
  CHECK(f.cursorCount == 1);
  CHECK(f.cursorVisible);

  f.shapeCount  = 0;
  f.colorCount  = 0;
  f.whiteCount  = 0;
  f.cursorCount = 0;
  renderQueue_sourceClearCursor(RENDER_QUEUE_SOURCE_PRIMARY);
  renderQueue_sourceInvalidate(RENDER_QUEUE_SOURCE_PRIMARY, generation);
  const uint64_t replacement =
    renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY);
  CHECK(replacement != generation);
  CHECK(f.shapeCount == 0);
  CHECK(f.colorCount == 0);
  CHECK(f.whiteCount == 0);
  CHECK(f.cursorCount == 0);
  renderQueue_process();
  CHECK(f.shapeCount == 0);
  CHECK(f.colorCount == 1);
  CHECK(f.colorFlags == 0);
  CHECK(f.whiteCount == 1);
  CHECK(f.whiteLevel == LG_SDR_WHITE_LEVEL_DEFAULT);
  CHECK(f.cursorCount == 1);
  CHECK(!f.cursorVisible);

  f.shapeCount  = 0;
  f.colorCount  = 0;
  f.whiteCount  = 0;
  f.cursorCount = 0;
  CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY,
        replacement, false, NULL) != 0);
  renderQueue_process();
  f.shapeCount  = 0;
  f.colorCount  = 0;
  f.whiteCount  = 0;
  f.cursorCount = 0;
  renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, replacement,
      LG_CURSOR_COLOR, 1, 1, 4, shape);
  renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, replacement,
      true, 20, 21, 3, 4);
  renderQueue_process();
  CHECK(f.shapeCount == 1);
  CHECK(f.cursorCount == 1);
  CHECK(f.cursorVisible);
  CHECK(f.cursorX == 20);
  CHECK(f.cursorY == 21);

  stop();
}

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

static const struct Test tests[] =
{
  { "generation", testGeneration },
  { "latest"    , testLatest     },
  { "prepare"   , testPrepare    },
  { "presented" , testPresented  },
  { "payload"   , testPayload    },
  { "validation", testValidation },
  { "cache"     , testCache      },
  { "cursor-replacement", testCursorReplacement },
};

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;
}
