/**
 * @file mockdevice.h
 * @brief Recording graphics, audio and input backends for the device tests.
 *
 * The graphics and audio verbs produce no output a golden file can compare, so
 * the assertions have to be about what reached the backend. These record every
 * call as a formatted line, which a test compares against the sequence it
 * expected -- what was drawn, in what order, in what color.
 *
 * Nothing here needs SDL, which is the point of the backend records in the first
 * place: the whole of groups G, I and E is testable in the default build.
 */

#ifndef _AKBASIC_TEST_MOCKDEVICE_H_
#define _AKBASIC_TEST_MOCKDEVICE_H_

#include <stdarg.h>
#include <stdio.h>
#include <string.h>

#include <akerror.h>

#include <akbasic/audio.h>
#include <akbasic/error.h>
#include <akbasic/graphics.h>
#include <akbasic/input.h>
#include <akbasic/sprite.h>
#include <akbasic/ui.h>

/** @brief Room for the call log. Longer than any test needs; overflow truncates loudly. */
#define MOCK_LOG_SIZE		8192
/** @brief How many keystrokes mock_input can be primed with. */
#define MOCK_MAX_KEYS		32
/** @brief How many regions mock_graphics will hand out handles for. */
#define MOCK_MAX_SHAPES		8

/** @brief State shared by all three mock backends, so one log records the lot. */
typedef struct
{
    char log[MOCK_LOG_SIZE];
    size_t used;
    bool overflowed;

    /* Graphics */
    int shapes;			/* handles handed out so far			*/
    bool paint_exhausts;	/* when true, paint reports a partial fill	*/
    int devwidth;		/* what size() reports; see mock_devices_init	*/
    int devheight;

    /* Audio */
    bool voice_active[AKBASIC_AUDIO_VOICES];

    /* Input */
    int keys[MOCK_MAX_KEYS];
    int keycount;
    int keynext;

    /* Sprites */
    uint16_t collisions;	/* what the next collisions() call reports	*/
    uint16_t solidcollisions;	/* and what the next solids() call reports	*/
    akbasic_Contact contact;	/* what the next contact() call describes	*/
    bool hascontact;
    int patternbytes[AKBASIC_MAX_SPRITES];  /* bytes the last define() carried, per sprite */

    /*
     * UI. The verbs are setters rather than draw calls, so what a test asserts
     * is both the call sequence *and* what the backend now holds -- a MENU that
     * logged correctly and stored nothing would still be broken.
     */
    int menuselected[AKBASIC_UI_MAX_MENUS];
    bool menuactivated[AKBASIC_UI_MAX_MENUS];
    int menucount[AKBASIC_UI_MAX_MENUS];
} akbasic_MockDevice;

static akbasic_MockDevice MOCK;
static akbasic_GraphicsBackend MOCK_GRAPHICS;
static akbasic_AudioBackend MOCK_AUDIO;
static akbasic_InputBackend MOCK_INPUT;
static akbasic_SpriteBackend MOCK_SPRITES;
static akbasic_UiBackend MOCK_UI;

/**
 * @brief Append one formatted call to the log.
 *
 * Deliberately not an akerr_ErrorContext * function. It is called from inside
 * the vtable entry points, and a test whose log overflowed should say so through
 * the assertion on the log contents rather than by raising an error that the
 * verb under test would then report as a device failure.
 */
__attribute__((format(printf, 1, 2)))
static void mock_log(const char *format, ...)
{
    va_list args;
    int written = 0;

    if ( MOCK.used >= sizeof(MOCK.log) - 1 ) {
	MOCK.overflowed = true;
	return;
    }
    va_start(args, format);
    written = vsnprintf(MOCK.log + MOCK.used, sizeof(MOCK.log) - MOCK.used, format, args);
    va_end(args);
    if ( written < 0 || (size_t)written >= sizeof(MOCK.log) - MOCK.used ) {
	MOCK.overflowed = true;
	MOCK.used = sizeof(MOCK.log) - 1;
	return;
    }
    MOCK.used += (size_t)written;
}

/* --------------------------------------------------------------- graphics -- */

/**
 * @brief Report the recorded surface size, and log that it was asked.
 *
 * Logged like every other call because *when* the interpreter asks matters: it
 * re-asks before each verb that draws, deliberately, so a resized window is not
 * drawn to at its old size.
 */
static akerr_ErrorContext *mock_size(akbasic_GraphicsBackend *self, int *width, int *height)
{
    PREPARE_ERROR(errctx);
    (void)self;
    FAIL_ZERO_RETURN(errctx, (width != NULL && height != NULL), AKERR_NULLPOINTER,
		     "NULL destination in mock_size");
    *width = MOCK.devwidth;
    *height = MOCK.devheight;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    (void)self;
    mock_log("point %.1f,%.1f #%02x%02x%02x\n", x, y, color.r, color.g, color.b);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_line(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    (void)self;
    mock_log("line %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    (void)self;
    mock_log("rect %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_filled_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    (void)self;
    mock_log("fillrect %.1f,%.1f-%.1f,%.1f #%02x%02x%02x\n", x1, y1, x2, y2, color.r, color.g, color.b);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_paint(akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    (void)self;
    mock_log("paint %d,%d #%02x%02x%02x\n", x, y, color.r, color.g, color.b);
    /*
     * Stands in for the akgl flood fill running out of its fixed span stack,
     * which reports AKERR_OUTOFBOUNDS having filled part of the region. PAINT has
     * to surface that rather than treat it as success.
     */
    FAIL_ZERO_RETURN(errctx, (!MOCK.paint_exhausts), AKERR_OUTOFBOUNDS,
		     "mock span stack exhausted");
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_clear(akbasic_GraphicsBackend *self, akbasic_Color color)
{
    PREPARE_ERROR(errctx);
    (void)self;
    mock_log("clear #%02x%02x%02x\n", color.r, color.g, color.b);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_save_shape(akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (handle != NULL), AKERR_NULLPOINTER, "NULL handle in save_shape");
    FAIL_ZERO_RETURN(errctx, (MOCK.shapes < MOCK_MAX_SHAPES), AKBASIC_ERR_DEVICE,
		     "mock shape pool exhausted");
    *handle = MOCK.shapes;
    MOCK.shapes += 1;
    mock_log("save %d,%d-%d,%d -> %d\n", x1, y1, x2, y2, *handle);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_paste_shape(akbasic_GraphicsBackend *self, int handle, double x, double y)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < MOCK.shapes), AKBASIC_ERR_BOUNDS,
		     "no such mock shape %d", handle);
    mock_log("paste %d at %.1f,%.1f\n", handle, x, y);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_free_shapes(akbasic_GraphicsBackend *self)
{
    PREPARE_ERROR(errctx);
    (void)self;
    MOCK.shapes = 0;
    mock_log("freeshapes\n");
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------------ audio -- */

static akerr_ErrorContext *mock_tone(akbasic_AudioBackend *self, int voice, double hz, int ms)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
		     "mock voice %d out of range", voice);
    MOCK.voice_active[voice] = true;
    mock_log("tone v%d %.1fhz %dms\n", voice, hz, ms);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_sweep(akbasic_AudioBackend *self, int voice, double from_hz, double to_hz, double step_hz, int ms)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
		     "mock voice %d out of range", voice);
    MOCK.voice_active[voice] = true;
    mock_log("sweep v%d %.1fhz->%.1fhz step %.1fhz %dms\n",
	     voice, from_hz, to_hz, step_hz, ms);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_audio_stop(akbasic_AudioBackend *self, int voice)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
		     "mock voice %d out of range", voice);
    MOCK.voice_active[voice] = false;
    mock_log("stop v%d\n", voice);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_waveform(akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
		     "mock voice %d out of range", voice);
    mock_log("wave v%d %d\n", voice, (int)waveform);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_envelope(akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
		     "mock voice %d out of range", voice);
    mock_log("env v%d %d/%d/%.2f/%d\n", voice, attack, decay, sustain, release);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_volume(akbasic_AudioBackend *self, double level)
{
    PREPARE_ERROR(errctx);
    (void)self;
    mock_log("vol %.2f\n", level);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_voice_active(akbasic_AudioBackend *self, int voice, bool *active)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (active != NULL), AKERR_NULLPOINTER, "NULL active in voice_active");
    FAIL_ZERO_RETURN(errctx, (voice >= 0 && voice < AKBASIC_AUDIO_VOICES), AKBASIC_ERR_BOUNDS,
		     "mock voice %d out of range", voice);
    *active = MOCK.voice_active[voice];
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------------ input -- */

static akerr_ErrorContext *mock_poll_key(akbasic_InputBackend *self, int *keycode, bool *available)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (keycode != NULL && available != NULL), AKERR_NULLPOINTER,
		     "NULL argument in poll_key");
    if ( MOCK.keynext >= MOCK.keycount ) {
	/* An empty buffer is success, not an error -- see input.h. */
	*available = false;
	*keycode = 0;
	SUCCEED_RETURN(errctx);
    }
    *keycode = MOCK.keys[MOCK.keynext];
    *available = true;
    MOCK.keynext += 1;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_flush_keys(akbasic_InputBackend *self)
{
    PREPARE_ERROR(errctx);
    (void)self;
    MOCK.keynext = MOCK.keycount;
    mock_log("flushkeys\n");
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- sprites -- */

/**
 * @brief Refuse a sprite number the way a real backend has to.
 *
 * Sprites are 1-based at this boundary -- see sprite.h -- so an off-by-one in a
 * verb handler shows up here as AKBASIC_ERR_BOUNDS rather than as a silently
 * mis-addressed sprite.
 */
static akerr_ErrorContext AKERR_NOIGNORE *mock_sprite_range(int n)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_MAX_SPRITES), AKBASIC_ERR_BOUNDS,
		     "mock sprite %d out of range", n);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_define(akbasic_SpriteBackend *self, int n, const uint8_t *pattern, int bytes, akbasic_Color fg, akbasic_Color bg)
{
    PREPARE_ERROR(errctx);
    (void)self;

    PASS(errctx, mock_sprite_range(n));
    FAIL_ZERO_RETURN(errctx, (pattern != NULL), AKERR_NULLPOINTER, "NULL pattern in define");
    MOCK.patternbytes[n - 1] = bytes;
    /*
     * The first and last bytes rather than all 63: enough to tell one pattern
     * from another and to catch a byte order that came through reversed,
     * without a log line no human can read.
     */
    mock_log("sprdef %d %d bytes %02x..%02x fg=%d,%d,%d bg=%d,%d,%d\n",
	     n, bytes, pattern[0], pattern[bytes - 1],
	     fg.r, fg.g, fg.b, bg.r, bg.g, bg.b);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_define_shape(akbasic_SpriteBackend *self, int n, int handle)
{
    PREPARE_ERROR(errctx);
    (void)self;

    PASS(errctx, mock_sprite_range(n));
    mock_log("sprshape %d from %d\n", n, handle);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_define_file(akbasic_SpriteBackend *self, int n, const char *path, const char *root)
{
    PREPARE_ERROR(errctx);
    (void)self;

    PASS(errctx, mock_sprite_range(n));
    FAIL_ZERO_RETURN(errctx, (path != NULL), AKERR_NULLPOINTER, "NULL path in define_file");
    /*
     * The root is logged as well as the path. Whether the interpreter passed the
     * running program's directory through is the whole of what this repository
     * owns about relative asset paths -- resolving one is libakgl's job, and
     * there is no way to see it happen from a mock.
     */
    mock_log("sprfile %d %s root=%s\n", n, path, (root != NULL ? root : "(none)"));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_show(akbasic_SpriteBackend *self, int n, bool visible)
{
    PREPARE_ERROR(errctx);
    (void)self;

    PASS(errctx, mock_sprite_range(n));
    mock_log("sprshow %d %d\n", n, (visible ? 1 : 0));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_move(akbasic_SpriteBackend *self, int n, double x, double y)
{
    PREPARE_ERROR(errctx);
    (void)self;

    PASS(errctx, mock_sprite_range(n));
    mock_log("sprmove %d %.2f,%.2f\n", n, x, y);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_configure(akbasic_SpriteBackend *self, int n, akbasic_Color color, bool xexpand, bool yexpand, bool behind)
{
    PREPARE_ERROR(errctx);
    (void)self;

    PASS(errctx, mock_sprite_range(n));
    mock_log("sprcfg %d %d,%d,%d x%d y%d b%d\n", n, color.r, color.g, color.b,
	     (xexpand ? 1 : 0), (yexpand ? 1 : 0), (behind ? 1 : 0));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_shared_colors(akbasic_SpriteBackend *self, akbasic_Color c1, akbasic_Color c2)
{
    PREPARE_ERROR(errctx);
    (void)self;

    mock_log("sprshared %d,%d,%d %d,%d,%d\n", c1.r, c1.g, c1.b, c2.r, c2.g, c2.b);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions");
    *mask = MOCK.collisions;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_solids(akbasic_SpriteBackend *self, uint16_t *mask)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in solids");
    *mask = MOCK.solidcollisions;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_shape(akbasic_SpriteBackend *self, int n, int kind,
					  double x1, double y1, double x2, double y2)
{
    PREPARE_ERROR(errctx);
    (void)self;

    mock_log("sprhit %d,%d,%g,%g,%g,%g\n", n, kind, x1, y1, x2, y2);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_solid(akbasic_SpriteBackend *self, int id, bool define,
					  double x1, double y1, double x2, double y2)
{
    PREPARE_ERROR(errctx);
    (void)self;

    if ( !define ) {
	mock_log("solid %d off\n", id);
	SUCCEED_RETURN(errctx);
    }
    mock_log("solid %d,%g,%g,%g,%g\n", id, x1, y1, x2, y2);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_spr_contact(akbasic_SpriteBackend *self, int n,
					    akbasic_Contact *dest, bool *found)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (dest != NULL && found != NULL), AKERR_NULLPOINTER,
		     "NULL argument in contact");
    *found = MOCK.hascontact;
    if ( *found ) {
	*dest = MOCK.contact;
	dest->other = n;
    }
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- fixture -- */

/* --------------------------------------------------------------------- ui -- */

static akerr_ErrorContext *mock_ui_dialog(akbasic_UiBackend *self, const char *text)
{
    PREPARE_ERROR(errctx);
    (void)self;
    mock_log("dialog %s\n", (text != NULL) ? text : "(none)");
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_ui_label(akbasic_UiBackend *self, int slot, int anchor, const char *text)
{
    PREPARE_ERROR(errctx);
    (void)self;
    mock_log("label %d anchor %d %s\n", slot, anchor, (text != NULL) ? text : "(none)");
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_ui_menu(akbasic_UiBackend *self, int slot, const char *const *items, int count)
{
    PREPARE_ERROR(errctx);
    int i = 0;
    (void)self;

    FAIL_ZERO_RETURN(errctx, (slot >= 0 && slot < AKBASIC_UI_MAX_MENUS), AKERR_OUTOFBOUNDS,
		     "mock menu slot %d", slot);
    mock_log("menu %d count %d", slot, count);
    for ( i = 0; i < count; i++ ) {
	mock_log(" [%s]", (items != NULL && items[i] != NULL) ? items[i] : "(null)");
    }
    mock_log("\n");
    MOCK.menucount[slot] = count;
    MOCK.menuselected[slot] = 0;
    MOCK.menuactivated[slot] = false;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_ui_menu_state(akbasic_UiBackend *self, int slot, int *selected, bool *activated, bool clear)
{
    PREPARE_ERROR(errctx);
    (void)self;

    FAIL_ZERO_RETURN(errctx, (selected != NULL && activated != NULL), AKERR_NULLPOINTER,
		     "NULL destination in mock_ui_menu_state");
    FAIL_ZERO_RETURN(errctx, (slot >= 0 && slot < AKBASIC_UI_MAX_MENUS), AKERR_OUTOFBOUNDS,
		     "mock menu slot %d", slot);
    *selected = MOCK.menuselected[slot];
    *activated = MOCK.menuactivated[slot];
    if ( clear ) {
	MOCK.menuactivated[slot] = false;
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_ui_style(akbasic_UiBackend *self, akbasic_Color *fill, akbasic_Color *edge, akbasic_Color *ink, double padding, double radius)
{
    PREPARE_ERROR(errctx);
    (void)self;

    if ( fill == NULL || edge == NULL || ink == NULL ) {
	mock_log("style default\n");
	SUCCEED_RETURN(errctx);
    }
    mock_log("style #%02x%02x%02x #%02x%02x%02x #%02x%02x%02x pad %.1f radius %.1f\n",
	     fill->r, fill->g, fill->b, edge->r, edge->g, edge->b,
	     ink->r, ink->g, ink->b, padding, radius);
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *mock_ui_clear(akbasic_UiBackend *self)
{
    PREPARE_ERROR(errctx);
    int i = 0;
    (void)self;

    mock_log("uiclear\n");
    for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
	MOCK.menucount[i] = 0;
	MOCK.menuselected[i] = 0;
	MOCK.menuactivated[i] = false;
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Stand in for the player choosing an entry.
 *
 * The one thing a mock has to be able to do that no verb can: a real activation
 * comes from a keystroke libakgl consumed, and there is no keyboard here.
 */
__attribute__((unused))
static void mock_menu_choose(int slot, int selected)
{
    if ( slot < 0 || slot >= AKBASIC_UI_MAX_MENUS ) {
	return;
    }
    MOCK.menuselected[slot] = selected;
    MOCK.menuactivated[slot] = true;
}

/** @brief Reset the recorder and populate all three vtables. */
__attribute__((unused))
static void mock_devices_init(void)
{
    memset(&MOCK, 0, sizeof(MOCK));
    /*
     * 640x400 rather than 320x200, on purpose: it is exactly twice the fallback,
     * so a scaled coordinate that came out right by accident -- because nothing
     * consulted the device at all -- is off by a factor of two rather than
     * indistinguishable. A test that wants the fallback clears MOCK_GRAPHICS.size,
     * the way the SOUND test clears MOCK_AUDIO.sweep.
     */
    MOCK.devwidth		= 640;
    MOCK.devheight		= 400;

    MOCK_GRAPHICS.self		= &MOCK;
    MOCK_GRAPHICS.size		= mock_size;
    MOCK_GRAPHICS.point		= mock_point;
    MOCK_GRAPHICS.line		= mock_line;
    MOCK_GRAPHICS.rect		= mock_rect;
    MOCK_GRAPHICS.filled_rect	= mock_filled_rect;
    MOCK_GRAPHICS.paint		= mock_paint;
    MOCK_GRAPHICS.clear		= mock_clear;
    MOCK_GRAPHICS.save_shape	= mock_save_shape;
    MOCK_GRAPHICS.paste_shape	= mock_paste_shape;
    MOCK_GRAPHICS.free_shapes	= mock_free_shapes;

    MOCK_AUDIO.self		= &MOCK;
    MOCK_AUDIO.tone		= mock_tone;
    /*
     * Set here, and deliberately cleared again by the one test that needs a
     * backend without it: `sweep` may be NULL, which is what a host written
     * against libakgl 0.2.0 looks like, and SOUND has to refuse rather than
     * crash on one.
     */
    MOCK_AUDIO.sweep		= mock_sweep;
    MOCK_AUDIO.stop		= mock_audio_stop;
    MOCK_AUDIO.waveform		= mock_waveform;
    MOCK_AUDIO.envelope		= mock_envelope;
    MOCK_AUDIO.volume		= mock_volume;
    MOCK_AUDIO.voice_active	= mock_voice_active;

    MOCK_INPUT.self		= &MOCK;
    MOCK_INPUT.poll_key		= mock_poll_key;
    MOCK_INPUT.flush_keys	= mock_flush_keys;

    MOCK_SPRITES.self		= &MOCK;
    MOCK_SPRITES.define		= mock_spr_define;
    MOCK_SPRITES.define_shape	= mock_spr_define_shape;
    MOCK_SPRITES.define_file	= mock_spr_define_file;
    MOCK_SPRITES.show		= mock_spr_show;
    MOCK_SPRITES.move		= mock_spr_move;
    MOCK_SPRITES.configure	= mock_spr_configure;
    MOCK_SPRITES.shared_colors	= mock_spr_shared_colors;
    MOCK_SPRITES.collisions	= mock_spr_collisions;
    MOCK_SPRITES.solids		= mock_spr_solids;
    MOCK_SPRITES.shape		= mock_spr_shape;
    MOCK_SPRITES.solid		= mock_spr_solid;
    MOCK_SPRITES.contact	= mock_spr_contact;

    MOCK_UI.self		= &MOCK;
    MOCK_UI.dialog		= mock_ui_dialog;
    MOCK_UI.label		= mock_ui_label;
    MOCK_UI.menu		= mock_ui_menu;
    MOCK_UI.menu_state		= mock_ui_menu_state;
    MOCK_UI.style		= mock_ui_style;
    MOCK_UI.clear		= mock_ui_clear;
}

/** @brief Set what the next collisions() call will report. Bit n-1 is sprite n. */
__attribute__((unused))
static void mock_set_collisions(uint16_t mask)
{
    MOCK.collisions = mask;
}

/** @brief Set what the next solids() call will report. Bit n-1 is sprite n. */
__attribute__((unused))
static void mock_set_solid_collisions(uint16_t mask)
{
    MOCK.solidcollisions = mask;
}

/** @brief Set the contact the next contact() call will report. */
__attribute__((unused))
static void mock_set_contact(int what, double nx, double ny, double depth, int axis)
{
    memset(&MOCK.contact, 0, sizeof(MOCK.contact));
    MOCK.contact.what = what;
    MOCK.contact.nx = nx;
    MOCK.contact.ny = ny;
    MOCK.contact.depth = depth;
    MOCK.contact.axis = axis;
    MOCK.hascontact = (what != 0);
}

/** @brief Prime the input backend with keystrokes GET will drain in order. */
__attribute__((unused))
static void mock_push_keys(const char *keys)
{
    size_t i = 0;

    MOCK.keycount = 0;
    MOCK.keynext = 0;
    for ( i = 0; keys != NULL && keys[i] != '\0' && MOCK.keycount < MOCK_MAX_KEYS; i++ ) {
	MOCK.keys[MOCK.keycount] = (int)(unsigned char)keys[i];
	MOCK.keycount += 1;
    }
}

/** @brief Forget everything logged so far, keeping the primed state. */
__attribute__((unused))
static void mock_log_reset(void)
{
    memset(MOCK.log, 0, sizeof(MOCK.log));
    MOCK.used = 0;
    MOCK.overflowed = false;
}

#endif // _AKBASIC_TEST_MOCKDEVICE_H_
