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

#include "common/KVMFR.h"
#include "common/LGMPConfig.h"
#include "common/debug.h"
#include "common/option.h"

#include <lgmp/host.h>

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

#define TEST_SHM_SIZE       (2U * 1024U * 1024U)
#define TEST_QUEUE_TIMEOUT  250U
#define TEST_WAIT_LOOPS     500U
#define TEST_FRAME_DATA     64U
#define TEST_MAX_ALLOCS     64U
#define TEST_FRAME_QUEUES   (LGMP_Q_FRAME_LEN + 1U)

extern const LG_TransportOps LGT_LGMP;

typedef struct TestFrame
{
  PLGMPMemory   memory;
  KVMFRFrame  * wire;
}
TestFrame;

typedef struct TestState
{
  int                 fd;
  char                path[64];
  bool                pathExists;
  void              * hostMemory;
  PLGMPHost           host;
  PLGMPHostQueue      queues[TEST_FRAME_QUEUES];
  PLGMPMemory         allocations[TEST_MAX_ALLOCS];
  unsigned            allocationCount;
  LG_Transport      * transport;
  const LG_FrameOps * frameOps;
}
TestState;

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

static bool waitForEmpty(TestState * state)
{
  for (unsigned attempt = 0; attempt < TEST_WAIT_LOOPS; ++attempt)
  {
    if (lgmpHostProcess(state->host) != LGMP_OK)
      return false;

    bool empty = true;
    for (unsigned i = 0; i < TEST_FRAME_QUEUES; ++i)
      empty &= lgmpHostQueuePending(state->queues[i]) == 0;
    if (empty)
      return true;

    usleep(1000);
  }

  return false;
}

static bool allocMemory(TestState * state, uint32_t size, uint32_t alignment,
    PLGMPMemory * memory, void ** pointer)
{
  CHECK(state->allocationCount < TEST_MAX_ALLOCS);
  CHECK(lgmpHostMemAllocAligned(
        state->host, size, alignment, memory) == LGMP_OK);
  state->allocations[state->allocationCount++] = *memory;
  *pointer = lgmpHostMemPtr(*memory);
  return true;
}

static bool newFrame(TestState * state, uint32_t serial, TestFrame * result)
{
  const uint32_t size = sizeof(KVMFRFrame) + sizeof(FrameBuffer) +
    TEST_FRAME_DATA;
  void * pointer;
  CHECK(allocMemory(state, size, _Alignof(KVMFRFrame),
        &result->memory, &pointer));

  result->wire = pointer;
  result->wire->formatVer     = 1;
  result->wire->frameSerial   = serial;
  result->wire->type          = FRAME_TYPE_BGRA;
  result->wire->screenWidth   = 4;
  result->wire->screenHeight  = 4;
  result->wire->dataWidth     = 4;
  result->wire->dataHeight    = 4;
  result->wire->frameWidth    = 4;
  result->wire->frameHeight   = 4;
  result->wire->rotation      = FRAME_ROT_0;
  result->wire->stride        = 4;
  result->wire->pitch         = 16;
  result->wire->offset        = sizeof(KVMFRFrame);
  result->wire->sdrWhiteLevel = KVMFR_SDR_WHITE_LEVEL_DEFAULT;

  FrameBuffer * framebuffer = (FrameBuffer *)((uint8_t *)result->wire +
      result->wire->offset);
  atomic_store(&framebuffer->wp, TEST_FRAME_DATA);
  return true;
}

static void setDamage(TestFrame * frame, uint32_t count)
{
  frame->wire->damageRectsCount = count;
  for (uint32_t i = 0; i < count && i < KVMFR_MAX_DAMAGE_RECTS; ++i)
  {
    frame->wire->damageRects[i].x      = i + 1;
    frame->wire->damageRects[i].y      = i + 2;
    frame->wire->damageRects[i].width  = i + 3;
    frame->wire->damageRects[i].height = i + 4;
  }
}

static bool postShared(TestState * state, const TestFrame * frame)
{
  CHECK(lgmpHostQueuePost(state->queues[0], 0, frame->memory) == LGMP_OK);
  return true;
}

static bool postOwner(TestState * state, unsigned lane, uint64_t token,
    const TestFrame * frame)
{
  CHECK(lane < LGMP_Q_FRAME_LEN);

  uint32_t       clientIDs[32];
  unsigned       clientCount;
  unsigned       recipientCount;
  PLGMPHostQueue queue = state->queues[lane + 1];
  CHECK(lgmpHostGetClientIDs(queue, clientIDs, &clientCount) == LGMP_OK);
  CHECK(clientCount == 1);
  CHECK(lgmpHostQueuePostForClients(queue, token, frame->memory,
        clientIDs, clientCount, &recipientCount) == LGMP_OK);
  CHECK(recipientCount == 1);
  return true;
}

static bool resetFrames(TestState * state)
{
  state->frameOps->stopFrame(state->transport);
  CHECK(waitForEmpty(state));

  LG_TransportFrame frame;
  CHECK(state->frameOps->nextFrame(state->transport, false, &frame) ==
      LG_TRANSPORT_TIMEOUT);
  for (unsigned i = 0; i < TEST_FRAME_QUEUES; ++i)
    CHECK(lgmpHostQueueHasSubs(state->queues[i]));
  return true;
}

static bool releaseFrame(TestState * state, LG_TransportFrame * frame)
{
  state->frameOps->releaseFrame(state->transport, frame);
  CHECK(frame->releaseFn == NULL);
  CHECK(frame->releaseHandle == 0);
  CHECK(waitForEmpty(state));
  return true;
}

static bool testMalformed(TestState * state)
{
  CHECK(resetFrames(state));

  PLGMPMemory shortMemory;
  void * shortPointer;
  CHECK(allocMemory(state, sizeof(KVMFRFrame) - 4, _Alignof(uint32_t),
        &shortMemory,
        &shortPointer));
  CHECK(lgmpHostQueuePost(state->queues[0], 0, shortMemory) == LGMP_OK);
  LG_TransportFrame result;
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_ERROR);
  CHECK(waitForEmpty(state));

  TestFrame invalidType;
  CHECK(newFrame(state, 1, &invalidType));
  invalidType.wire->type = FRAME_TYPE_INVALID;
  CHECK(postShared(state, &invalidType));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_ERROR);
  CHECK(waitForEmpty(state));

  TestFrame invalidOffset;
  CHECK(newFrame(state, 2, &invalidOffset));
  invalidOffset.wire->offset = UINT32_MAX;
  CHECK(postShared(state, &invalidOffset));
  CHECK(state->frameOps->nextFrame(state->transport, true, &result) ==
      LG_TRANSPORT_ERROR);
  CHECK(waitForEmpty(state));

  TestFrame invalidSize;
  CHECK(newFrame(state, 3, &invalidSize));
  invalidSize.wire->dataHeight = UINT32_MAX;
  invalidSize.wire->pitch      = UINT32_MAX;
  CHECK(postShared(state, &invalidSize));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_ERROR);
  CHECK(waitForEmpty(state));

  TestFrame malformed;
  TestFrame valid;
  CHECK(newFrame(state, 4, &malformed));
  CHECK(newFrame(state, 4, &valid));
  malformed.wire->offset               = UINT32_MAX;
  valid.wire->formatVer                = 44;
  valid.wire->scheduleEpoch            = 2;
  valid.wire->scheduleDeadlineSerial   = 9;
  CHECK(postShared(state, &malformed));
  CHECK(postOwner(state, 0, (UINT64_C(2) << 32) | 9, &valid));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == 4);
  CHECK(result.scheduleOwner);
  CHECK(result.format && result.format->version == 44);
  CHECK(releaseFrame(state, &result));
  return true;
}

static bool testSerialDamage(TestState * state)
{
  CHECK(resetFrames(state));

  LG_TransportFrame result;
  TestFrame first;
  CHECK(newFrame(state, UINT32_MAX - 1U, &first));
  setDamage(&first, 2);
  CHECK(postShared(state, &first));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == UINT32_MAX - 1U);
  CHECK(result.damageRects == NULL);
  CHECK(result.damageRectsCount == 0);
  CHECK(releaseFrame(state, &result));

  TestFrame last;
  CHECK(newFrame(state, UINT32_MAX, &last));
  setDamage(&last, 2);
  last.wire->flags = FRAME_FLAG_BLOCK_SCREENSAVER |
    FRAME_FLAG_REQUEST_ACTIVATION | FRAME_FLAG_TRUNCATED;
  CHECK(postShared(state, &last));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == UINT32_MAX);
  CHECK(result.damageRectsCount == 2);
  CHECK(result.damageRects);
  CHECK(result.damageRects[1].x == 2);
  CHECK(result.damageRects[1].height == 5);
  CHECK(result.flags == (LG_TRANSPORT_FRAME_BLOCK_SCREENSAVER |
        LG_TRANSPORT_FRAME_REQUEST_ACTIVATION |
        LG_TRANSPORT_FRAME_TRUNCATED));
  CHECK(releaseFrame(state, &result));

  TestFrame wrapped;
  CHECK(newFrame(state, 0, &wrapped));
  setDamage(&wrapped, 1);
  CHECK(postShared(state, &wrapped));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == 0);
  CHECK(result.damageRectsCount == 1);
  CHECK(releaseFrame(state, &result));

  TestFrame stale;
  CHECK(newFrame(state, UINT32_MAX, &stale));
  CHECK(postShared(state, &stale));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_TIMEOUT);
  CHECK(waitForEmpty(state));

  TestFrame duplicate;
  CHECK(newFrame(state, 0, &duplicate));
  CHECK(postShared(state, &duplicate));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_TIMEOUT);
  CHECK(waitForEmpty(state));

  TestFrame gap;
  CHECK(newFrame(state, 2, &gap));
  setDamage(&gap, 2);
  CHECK(postShared(state, &gap));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.damageRects == NULL);
  CHECK(result.damageRectsCount == 0);
  CHECK(releaseFrame(state, &result));

  TestFrame badDamage;
  CHECK(newFrame(state, 3, &badDamage));
  setDamage(&badDamage, KVMFR_MAX_DAMAGE_RECTS + 1U);
  CHECK(postShared(state, &badDamage));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.damageRects == NULL);
  CHECK(result.damageRectsCount == 0);
  CHECK(releaseFrame(state, &result));
  return true;
}

static bool testLaneSelection(TestState * state)
{
  CHECK(resetFrames(state));

  LG_TransportFrame result;
  TestFrame shared;
  TestFrame owner;
  CHECK(newFrame(state, 10, &shared));
  CHECK(newFrame(state, 9, &owner));
  shared.wire->formatVer = 10;
  owner.wire->formatVer  = 9;
  CHECK(postShared(state, &shared));
  CHECK(postOwner(state, 0, 1, &owner));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == 10);
  CHECK(!result.scheduleOwner);
  CHECK(result.format && result.format->version == 10);
  CHECK(releaseFrame(state, &result));

  TestFrame tiedShared;
  TestFrame tiedOwner;
  CHECK(newFrame(state, 11, &tiedShared));
  CHECK(newFrame(state, 11, &tiedOwner));
  tiedShared.wire->formatVer = 11;
  tiedOwner.wire->formatVer  = 12;
  CHECK(postShared(state, &tiedShared));
  CHECK(postOwner(state, 0, (UINT64_C(5) << 32) | 7, &tiedOwner));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == 11);
  CHECK(result.scheduleOwner);
  CHECK(result.scheduleEpoch == 5);
  CHECK(result.scheduleDeadlineSerial == 7);
  CHECK(result.format && result.format->version == 12);
  CHECK(releaseFrame(state, &result));

  TestFrame wrongDeadline;
  TestFrame matchingDeadline;
  CHECK(newFrame(state, 12, &wrongDeadline));
  CHECK(newFrame(state, 12, &matchingDeadline));
  wrongDeadline.wire->formatVer                 = 21;
  wrongDeadline.wire->scheduleEpoch             = 7;
  wrongDeadline.wire->scheduleDeadlineSerial    = 9;
  matchingDeadline.wire->formatVer              = 22;
  matchingDeadline.wire->scheduleEpoch          = 7;
  matchingDeadline.wire->scheduleDeadlineSerial = 9;
  CHECK(postOwner(state, 0, (UINT64_C(6) << 32) | 9, &wrongDeadline));
  CHECK(postOwner(state, 1, (UINT64_C(7) << 32) | 9, &matchingDeadline));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == 12);
  CHECK(result.scheduleEpoch == 7);
  CHECK(result.scheduleDeadlineSerial == 9);
  CHECK(result.format && result.format->version == 22);
  CHECK(releaseFrame(state, &result));

  TestFrame newerShared;
  TestFrame olderOwner;
  CHECK(newFrame(state, 13, &newerShared));
  CHECK(newFrame(state, 12, &olderOwner));
  newerShared.wire->formatVer = 31;
  olderOwner.wire->formatVer  = 32;
  CHECK(postShared(state, &newerShared));
  CHECK(postOwner(state, 0, 10, &olderOwner));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == 13);
  CHECK(!result.scheduleOwner);
  CHECK(result.format && result.format->version == 31);
  CHECK(releaseFrame(state, &result));

  TestFrame sharedOld;
  TestFrame ownerNew;
  CHECK(newFrame(state, 14, &sharedOld));
  CHECK(newFrame(state, 15, &ownerNew));
  sharedOld.wire->formatVer = 41;
  ownerNew.wire->formatVer  = 42;
  CHECK(postShared(state, &sharedOld));
  CHECK(postOwner(state, 0, 11, &ownerNew));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == 15);
  CHECK(result.scheduleOwner);
  CHECK(result.format && result.format->version == 42);
  CHECK(releaseFrame(state, &result));

  TestFrame duplicateShared;
  CHECK(newFrame(state, 15, &duplicateShared));
  CHECK(postShared(state, &duplicateShared));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_TIMEOUT);
  CHECK(waitForEmpty(state));

  TestFrame duplicateOwner;
  CHECK(newFrame(state, 15, &duplicateOwner));
  setDamage(&duplicateOwner, 1);
  CHECK(postOwner(state, 0, 12, &duplicateOwner));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.serial == 15);
  CHECK(result.scheduleOwner);
  CHECK(result.damageRects == NULL);
  CHECK(result.damageRectsCount == 0);
  CHECK(releaseFrame(state, &result));
  return true;
}

static bool testLeases(TestState * state)
{
  CHECK(resetFrames(state));

  TestFrame         first;
  TestFrame         second;
  LG_TransportFrame firstResult;
  LG_TransportFrame secondResult;
  CHECK(newFrame(state, 30, &first));
  CHECK(newFrame(state, 31, &second));
  CHECK(postShared(state, &first));
  CHECK(state->frameOps->nextFrame(state->transport, false, &firstResult) ==
      LG_TRANSPORT_OK);
  CHECK(firstResult.releaseFn);
  CHECK(firstResult.releaseHandle != 0);
  CHECK(lgmpHostQueueMessagePending(state->queues[0], first.memory, 0));

  CHECK(postShared(state, &second));
  CHECK(state->frameOps->nextFrame(state->transport, false, &secondResult) ==
      LG_TRANSPORT_TIMEOUT);
  CHECK(lgmpHostQueueMessagePending(state->queues[0], second.memory, 0));

  firstResult.releaseFn(firstResult.releaseOpaque, firstResult.releaseHandle);
  firstResult.releaseFn(firstResult.releaseOpaque, firstResult.releaseHandle);
  CHECK(state->frameOps->nextFrame(state->transport, false, &secondResult) ==
      LG_TRANSPORT_OK);
  CHECK(secondResult.serial == 31);
  CHECK(!lgmpHostQueueMessagePending(state->queues[0], first.memory, 0));
  state->frameOps->releaseFrame(state->transport, &firstResult);
  CHECK(releaseFrame(state, &secondResult));

  TestFrame stopped;
  LG_TransportFrame stoppedResult;
  CHECK(newFrame(state, 32, &stopped));
  CHECK(postShared(state, &stopped));
  CHECK(state->frameOps->nextFrame(state->transport, false, &stoppedResult) ==
      LG_TRANSPORT_OK);
  CHECK(stoppedResult.releaseFn);
  state->frameOps->stopFrame(state->transport);
  for (unsigned i = 0; i < TEST_FRAME_QUEUES; ++i)
    CHECK(!lgmpHostQueueHasSubs(state->queues[i]));
  stoppedResult.releaseFn(stoppedResult.releaseOpaque,
      stoppedResult.releaseHandle);
  stoppedResult.releaseFn(stoppedResult.releaseOpaque,
      stoppedResult.releaseHandle);
  state->frameOps->releaseFrame(state->transport, &stoppedResult);
  CHECK(waitForEmpty(state));

  CHECK(resetFrames(state));
  TestFrame resent;
  CHECK(newFrame(state, 32, &resent));
  CHECK(postShared(state, &resent));
  CHECK(state->frameOps->nextFrame(state->transport, false, &secondResult) ==
      LG_TRANSPORT_OK);
  CHECK(secondResult.serial == 32);
  CHECK(releaseFrame(state, &secondResult));

  TestFrame disconnected;
  LG_TransportFrame disconnectedResult;
  CHECK(newFrame(state, 33, &disconnected));
  CHECK(postShared(state, &disconnected));
  CHECK(state->frameOps->nextFrame(state->transport, false,
        &disconnectedResult) == LG_TRANSPORT_OK);
  CHECK(disconnectedResult.releaseFn);
  LGT_LGMP.disconnect(state->transport);
  CHECK(!LGT_LGMP.sessionValid(state->transport));
  disconnectedResult.releaseFn(disconnectedResult.releaseOpaque,
      disconnectedResult.releaseHandle);
  state->frameOps->releaseFrame(state->transport, &disconnectedResult);
  CHECK(waitForEmpty(state));
  return true;
}

static bool testDMAUnavailable(TestState * state)
{
  CHECK(resetFrames(state));
  CHECK(!state->frameOps->supportsDMA(state->transport));

  TestFrame         frame;
  LG_TransportFrame result;
  CHECK(newFrame(state, 50, &frame));
  CHECK(postShared(state, &frame));
  CHECK(state->frameOps->nextFrame(state->transport, false, &result) ==
      LG_TRANSPORT_OK);
  CHECK(result.dmaFD == -1);
  CHECK(releaseFrame(state, &result));
  return true;
}

static bool stateInit(TestState * state)
{
  memset(state, 0, sizeof(*state));
  state->fd         = -1;
  state->hostMemory = MAP_FAILED;
  memcpy(state->path, "/tmp/lgmp-frame-test-XXXXXX",
      sizeof("/tmp/lgmp-frame-test-XXXXXX"));

  state->fd = mkstemp(state->path);
  CHECK(state->fd >= 0);
  state->pathExists = true;
  CHECK(ftruncate(state->fd, TEST_SHM_SIZE) == 0);
  state->hostMemory = mmap(NULL, TEST_SHM_SIZE,
      PROT_READ | PROT_WRITE, MAP_SHARED, state->fd, 0);
  CHECK(state->hostMemory != MAP_FAILED);

  KVMFR session = { 0 };
  memcpy(session.magic, KVMFR_MAGIC, sizeof(session.magic));
  session.version  = KVMFR_VERSION;
  session.features = KVMFR_FEATURE_FRAME_SCHEDULE;
  memcpy(session.hostver, "frame-test", sizeof("frame-test"));
  CHECK(lgmpHostInit(state->hostMemory, TEST_SHM_SIZE, &state->host,
        sizeof(session), (uint8_t *)&session) == LGMP_OK);

  for (unsigned i = 0; i < TEST_FRAME_QUEUES; ++i)
  {
    const struct LGMPQueueConfig config = {
      .queueID     = i == 0 ? LGMP_Q_FRAME : LGMP_Q_FRAME_OWNER + i - 1,
      .numMessages = LGMP_Q_FRAME_LEN,
      .subTimeout  = TEST_QUEUE_TIMEOUT,
    };
    CHECK(lgmpHostQueueNew(state->host, config, &state->queues[i]) ==
        LGMP_OK);
  }

  LGT_LGMP.setup();
  option_set_string("lgmp", "shmDevice", state->path);
  option_set_bool("lgmp", "allowDMA", true);
  option_set_int("lgmp", "framePollInterval", 0);
  option_set_int("lgmp", "cursorPollInterval", 0);
  CHECK(LGT_LGMP.create(&state->transport));

  CHECK(unlink(state->path) == 0);
  state->pathExists = false;
  usleep(300000);
  CHECK(lgmpHostProcess(state->host) == LGMP_OK);

  LG_TransportSession transportSession;
  CHECK(LGT_LGMP.connect(state->transport, &transportSession) ==
      LG_TRANSPORT_OK);
  CHECK(transportSession.features & LG_TRANSPORT_FEATURE_FRAME_SCHEDULE);
  const LG_VideoOps * videoOps = LGT_LGMP.getVideoOps(state->transport);
  CHECK(videoOps && videoOps->type == LG_VIDEO_TYPE_FRAME && videoOps->frame);
  state->frameOps = videoOps->frame;
  CHECK(state->frameOps->stopFrame);
  return true;
}

static void stateFree(TestState * state)
{
  if (state->transport)
    LGT_LGMP.destroy(&state->transport);
  option_free();
  for (unsigned i = 0; i < state->allocationCount; ++i)
    lgmpHostMemFree(&state->allocations[i]);
  lgmpHostFree(&state->host);
  if (state->hostMemory != MAP_FAILED)
    munmap(state->hostMemory, TEST_SHM_SIZE);
  if (state->fd >= 0)
    close(state->fd);
  if (state->pathExists)
    unlink(state->path);
}

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

static const struct
{
  const char * name;
  TestFn       run;
}
tests[] =
{
  { "malformed"    , testMalformed      },
  { "serial-damage", testSerialDamage   },
  { "lanes"        , testLaneSelection  },
  { "leases"       , testLeases         },
  { "dma"          , testDMAUnavailable },
};

int main(int argc, char * argv[])
{
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s <malformed|serial-damage|lanes|leases|dma>\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;
  const bool initialized = stateInit(&state);
  const bool passed      = initialized && test(&state);

  stateFree(&state);
  return passed ? 0 : 1;
}
