/**
 * @file akgl_frontend.c
 * @brief Tests the standalone SDL frontend: the host, not the adaptors.
 *
 * tests/akgl_backends.c stands in for a host and asserts that each adaptor
 * reaches the right libakgl call. This file asserts the thing that was missing
 * until the frontend existed: that there *is* a host, that it composes the two
 * output paths so a program's bytes reach the window and stdout alike, that its
 * event pump feeds the keystroke ring a script reads, that its line editor turns
 * keystrokes into typed lines, and that closing the window ends a program that
 * would otherwise never stop.
 *
 * Everything runs under the dummy video and audio drivers with a software
 * renderer, following deps/libakgl/tests/draw.c, so it needs no display and no
 * sound card. The font is the reference's own Commodore one -- the acceptance
 * criterion in TODO.md section 3 asks for the drawn text to be in that font, and
 * the frontend is the only place that opens it.
 *
 * Only one frontend may exist at a time: libakgl's renderer and window are
 * process globals, and a host that never calls akgl_game_init() populates them
 * itself. Each test therefore brings one up and takes it down again.
 */

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

#include <SDL3/SDL.h>

#include <akerror.h>

#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/controller.h>
#include <akgl/draw.h>
#include <akgl/error.h>
/* game.h for the `akgl_camera` global, which akgl_actor_render() reads. */
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/sprite.h>

#include <akbasic/error.h>
#include <akbasic/frontend.h>
#include <akbasic/runtime.h>

#include "testutil.h"

/** @brief Window size. Small enough to read the whole target back cheaply. */
#define WINDOW_W	320
/** @brief Window height. */
#define WINDOW_H	200

/* Both of these carry pools far too large for a stack. */
static akbasic_AkglFrontend FRONTEND;
static akbasic_Runtime RUNTIME;
static char OUTPUT[8192];
static FILE *MIRROR = NULL;
static FILE *PROGRAM = NULL;

/** @brief Report whether any pixel in a rectangle is not the background. */
static bool anything_drawn(SDL_Surface *shot, int x0, int y0, int w, int h)
{
    uint8_t r = 0;
    uint8_t g = 0;
    uint8_t b = 0;
    uint8_t a = 0;
    int x = 0;
    int y = 0;

    if ( shot == NULL ) {
	return false;
    }
    for ( y = y0; y < y0 + h; y++ ) {
	for ( x = x0; x < x0 + w; x++ ) {
	    if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) {
		continue;
	    }
	    if ( r != 0 || g != 0 || b != 0 ) {
		return true;
	    }
	}
    }
    return false;
}

/**
 * @brief Queue one key-down, exactly as a keyboard would.
 *
 * Straight into SDL's own queue rather than into libakgl's ring, so the
 * frontend's event pump is the thing under test: nothing reaches the
 * interpreter unless pump_events() drained this and handed it over.
 *
 * Key-down only, with no composed text -- which is what a cursor key, a function
 * key or a bare modifier produces.
 */
static void push_key(int keycode)
{
    SDL_Event event;

    memset(&event, 0, sizeof(event));
    event.type = SDL_EVENT_KEY_DOWN;
    event.key.key = keycode;
    SDL_PushEvent(&event);
}

/**
 * @brief Queue a printable keystroke: the key-down *and* the text it composed to.
 *
 * Two events, because that is what a real keyboard produces and what libakgl
 * assembles a keystroke from -- SDL_EVENT_KEY_DOWN records the press and
 * SDL_EVENT_TEXT_INPUT delivers the character the layout, the shift state, a
 * compose key and a dead key between them worked out. A keycode alone cannot
 * express any of that, which is why the ring carries both.
 *
 * `text` is deliberately allowed to differ from `keycode`: that is how a shifted
 * character and a non-US layout are tested at all.
 */
static void push_text_key(int keycode, const char *text)
{
    SDL_Event event;

    push_key(keycode);
    memset(&event, 0, sizeof(event));
    event.type = SDL_EVENT_TEXT_INPUT;
    event.text.text = text;
    SDL_PushEvent(&event);
}

/**
 * @brief A one-character string with static storage, for one printable byte.
 *
 * SDL_PushEvent copies the SDL_Event but *not* the string an
 * SDL_EVENT_TEXT_INPUT points at, so the text has to outlive the call that
 * queued it and must not be reused before the queue is drained. A local buffer
 * is neither: the first attempt at push_line() used one and every event in the
 * line ended up pointing at the same overwritten two bytes, which showed up as
 * a REPL that never received a line and a test that hung rather than failed.
 */
static const char *one_char(unsigned char c)
{
    static char table[128][2];
    static bool built = false;
    int i = 0;

    if ( !built ) {
	for ( i = 0; i < 128; i++ ) {
	    table[i][0] = (char)i;
	    table[i][1] = '\0';
	}
	built = true;
    }
    return table[c & 0x7f];
}

/** @brief Queue a whole typed line, terminator included. */
static void push_line(const char *text)
{
    size_t i = 0;

    for ( i = 0; text[i] != '\0'; i++ ) {
	push_text_key((int)(unsigned char)text[i], one_char((unsigned char)text[i]));
    }
    push_key('\r');
}

/** @brief Queue the event a window manager sends when the close button is hit. */
static void push_quit(void)
{
    SDL_Event event;

    memset(&event, 0, sizeof(event));
    event.type = SDL_EVENT_QUIT;
    SDL_PushEvent(&event);
}

/**
 * @brief Bring up a frontend whose mirror is a buffer this file can read.
 *
 * @param source A program to feed through the stdio half, or NULL to type it.
 */
static akerr_ErrorContext AKERR_NOIGNORE *start_frontend(const char *source)
{
    PREPARE_ERROR(errctx);

    memset(OUTPUT, 0, sizeof(OUTPUT));
    MIRROR = fmemopen(OUTPUT, sizeof(OUTPUT), "w");
    FAIL_ZERO_RETURN(errctx, (MIRROR != NULL), AKERR_IO, "could not open the mirror buffer");
    setvbuf(MIRROR, NULL, _IONBF, 0);

    PROGRAM = NULL;
    if ( source != NULL ) {
	PROGRAM = fmemopen((void *)(uintptr_t)source, strlen(source), "r");
	FAIL_ZERO_RETURN(errctx, (PROGRAM != NULL), AKERR_IO,
			 "could not open the program buffer");
    }

    PASS(errctx, akbasic_frontend_akgl_init(&FRONTEND, "akbasic frontend test",
					    WINDOW_W, WINDOW_H,
					    AKBASIC_TEST_C64_FONT,
					    AKBASIC_FRONTEND_FONT_SIZE,
					    MIRROR, PROGRAM));
    PASS(errctx, akbasic_frontend_akgl_attach(&FRONTEND, &RUNTIME));
    SUCCEED_RETURN(errctx);
}

static void stop_frontend(void)
{
    akbasic_frontend_akgl_shutdown(&FRONTEND);
    if ( MIRROR != NULL ) {
	fclose(MIRROR);
	MIRROR = NULL;
    }
    if ( PROGRAM != NULL ) {
	fclose(PROGRAM);
	PROGRAM = NULL;
    }
}

/**
 * @brief A program run through the frontend reaches stdout *and* the window.
 *
 * This is the assertion the whole frontend exists for, and it is two assertions
 * on purpose: the mirrored bytes are what the golden corpus compares, and the
 * lit pixels are what a user actually sees. Before the frontend existed an AKGL
 * build produced the first and not the second.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_program_reaches_both(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;

    PASS(errctx, start_frontend("10 PRINT \"HELLO\"\n20 QUIT\n"));
    PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUNSTREAM));
    PASS(errctx, akbasic_frontend_akgl_drive(&FRONTEND, &RUNTIME));

    /* Byte for byte, exactly what the stdio-only driver would have written. */
    TEST_REQUIRE_STR(OUTPUT, "HELLO\n");
    /* And the same characters are in the grid the window draws from. */
    TEST_REQUIRE_STR(FRONTEND.akglstate.text[0], "HELLO");

    /*
     * Asserted as "the first five cells have something in them and the sixth
     * does not", rather than against particular pixels: which ones a glyph
     * lights is FreeType's business. What is being proved is that five
     * characters of Commodore font reached the renderer where the sink's cursor
     * said they would.
     */
    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");
    TEST_REQUIRE(anything_drawn(shot, 0, 0,
				5 * FRONTEND.akglstate.cellw,
				FRONTEND.akglstate.cellh),
		 "HELLO should have been drawn into the first five character cells");
    TEST_REQUIRE(!anything_drawn(shot, 0,
				 3 * FRONTEND.akglstate.cellh,
				 WINDOW_W, FRONTEND.akglstate.cellh),
		 "nothing was printed on the fourth row, so nothing should be drawn there");
    SDL_DestroySurface(shot);

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The event pump is what carries a keystroke to the interpreter.
 *
 * Pushed into SDL's queue and read back through the frontend's own input
 * backend, so every link in the chain the host owns is in the path: SDL queue,
 * pump_events, akgl_controller_handle_event, the ring, the backend.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_pump_feeds_input(void)
{
    PREPARE_ERROR(errctx);
    bool running = false;
    bool available = false;
    int keycode = 0;

    PASS(errctx, start_frontend("10 QUIT\n"));
    PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));

    /* Nothing pumped yet: the ring is empty and that is success, not an error. */
    PASS(errctx, FRONTEND.input.poll_key(&FRONTEND.input, &keycode, &available));
    TEST_REQUIRE(!available, "an unpumped frontend should have no keys waiting");

    push_key(SDLK_A);
    PASS(errctx, akbasic_frontend_akgl_pump(&FRONTEND, &running));
    TEST_REQUIRE(running, "a key event should not stop the frontend");

    PASS(errctx, FRONTEND.input.poll_key(&FRONTEND.input, &keycode, &available));
    TEST_REQUIRE(available, "the pump should have handed the key to the interpreter");
    TEST_REQUIRE_INT(keycode, SDLK_A);

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Closing the window stops a program that would otherwise never stop.
 *
 * `10 GOTO 10` is the case that matters: an unbounded run() would own the
 * process forever and the close button would do nothing. The bounded frame loop
 * is what makes this test finish at all.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_window_close_stops(void)
{
    PREPARE_ERROR(errctx);
    bool running = true;

    PASS(errctx, start_frontend("10 GOTO 10\n"));
    PASS(errctx, akbasic_runtime_load(&RUNTIME, "10 GOTO 10\n"));
    PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));

    push_quit();
    PASS(errctx, akbasic_frontend_akgl_drive(&FRONTEND, &RUNTIME));
    TEST_REQUIRE(!FRONTEND.running, "the close event should have stopped the frontend");
    TEST_REQUIRE(RUNTIME.mode != AKBASIC_MODE_QUIT,
		 "closing the window stops the host, it does not QUIT the script");

    /* And a pump after the close keeps reporting the same thing. */
    PASS(errctx, akbasic_frontend_akgl_pump(&FRONTEND, &running));
    TEST_REQUIRE(!running, "a closed frontend stays closed");

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The line editor turns keystrokes into a line, echoed as it is typed.
 *
 * Read through the sink directly rather than through the REPL, because what is
 * being asserted here is the editing itself: that the *composed text* is what
 * gets typed rather than the keycode, the backspace, the escape, and the echo
 * landing in the grid where the cursor says it is.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_line_editor(void)
{
    PREPARE_ERROR(errctx);
    char line[64];
    bool eof = true;

    PASS(errctx, start_frontend(NULL));
    PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));

    /* "prXnt" with the X backspaced away. Lower case survives, which it did not
     * before libakgl 0.3.0 -- the editor folded everything to upper case because
     * a keycode was all it had. */
    push_text_key('p', "p");
    push_text_key('r', "r");
    push_text_key('x', "x");
    push_key(SDLK_BACKSPACE);
    push_text_key('i', "i");
    push_text_key('n', "n");
    push_text_key('t', "t");
    push_key('\r');

    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE(!eof, "a submitted line is not end of input");
    TEST_REQUIRE_STR(line, "print");
    /* The echo is in the grid, and the backspace really removed a character. */
    TEST_REQUIRE_STR(FRONTEND.akglstate.text[0], "print");
    TEST_REQUIRE_INT(FRONTEND.akglstate.cursorrow, 1);
    TEST_REQUIRE_INT(FRONTEND.akglstate.cursorcol, 0);
    TEST_REQUIRE(!FRONTEND.akglstate.editing, "the editor should be idle once the line is in");

    /*
     * A shifted character, which is the whole point of the 0.3.0 keystroke API:
     * the key is still SDLK_2 and the character is '"'. Without the composed
     * text there is no way to tell those apart, and a BASIC string literal could
     * not be typed at the window at all.
     */
    push_text_key(SDLK_2, "\"");
    push_text_key('h', "H");
    push_text_key(SDLK_2, "\"");
    push_key('\r');
    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE_STR(line, "\"H\"");

    /*
     * And a key whose composed character has nothing to do with its keycode,
     * which is what a non-US layout looks like: AZERTY's SDLK_Q types "a".
     */
    push_text_key(SDLK_Q, "a");
    push_key('\r');
    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE_STR(line, "a");

    /* A key that composes to nothing is not typed: an arrow is not a character. */
    push_key(SDLK_UP);
    push_key(SDLK_LEFT);
    push_text_key('z', "z");
    push_key('\r');
    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE_STR(line, "z");

    /* Escape abandons a line rather than submitting it. */
    push_text_key('z', "z");
    push_key(SDLK_ESCAPE);
    push_text_key('o', "o");
    push_text_key('k', "k");
    push_key('\r');
    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE_STR(line, "ok");

    /* A closed window ends the wait, reported as end of input rather than raised. */
    push_quit();
    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE(eof, "closing the window during an INPUT is end of input");

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The frontend starts SDL text input, and the editor survives it not being on.
 *
 * **This is the regression test for a bug the rest of this file could not catch.**
 * Every other test here pushes SDL_EVENT_TEXT_INPUT into the queue by hand,
 * which is what a real keyboard produces -- but only once text input has been
 * *started*. SDL3 has it off by default and per-window, so a host that never
 * calls SDL_StartTextInput() gets no such events at all, every keystroke reaches
 * the ring with an empty `text`, and an editor that treats that as "not a
 * character" is silently dead. That is exactly what shipped: no echo in the
 * window, and nothing on stdout either, because RUN could never be typed.
 *
 * Two assertions, because there are two independent things wrong with that:
 * the frontend must turn text input on, and the editor must not be helpless if
 * it is ever off.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_text_input_is_started(void)
{
    PREPARE_ERROR(errctx);
    char line[64];
    bool eof = true;

    PASS(errctx, start_frontend(NULL));

    /* The frontend's job: SDL will not emit a text-input event without this. */
    TEST_REQUIRE(SDL_TextInputActive(FRONTEND.window),
		 "the frontend must start SDL text input, or nothing can be typed");

    /*
     * The editor's job: keystrokes carrying *no* composed text still type,
     * folded to upper case, which is all a bare keycode can offer. push_key()
     * sends the key-down alone, so this is precisely the shape the broken build
     * saw for every single key.
     */
    PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));
    push_key('r');
    push_key('u');
    push_key('n');
    push_key('\r');
    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE(!eof, "a text-less line is still a line");
    TEST_REQUIRE_STR(line, "RUN");
    TEST_REQUIRE_STR(FRONTEND.akglstate.text[0], "RUN");

    /* And a text-less key that is not printable is still not a character. */
    push_key(SDLK_F1);
    push_key(SDLK_RIGHT);
    push_key('a');
    push_key('\r');
    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE_STR(line, "A");

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/** @brief True when every pixel of one character cell is the background. */
static bool cell_is_blank(SDL_Surface *shot, akbasic_AkglSink *sink, int col, int row)
{
    return !anything_drawn(shot, sink->x + (col * sink->cellw),
			   sink->y + (row * sink->cellh),
			   sink->cellw, sink->cellh);
}

/**
 * @brief What the sink drew last frame is erased before it draws again.
 *
 * **Three reported symptoms, one cause.** The sink used to paint glyphs on top
 * of whatever was already on the renderer and never erase anything, and the
 * frontend deliberately does not clear the frame (§5 deviation 26, so a DRAW
 * survives). The grid was always right; the *screen* kept the previous frame:
 *
 *   - a backspaced character stayed on screen, so backspace looked broken
 *   - a scrolled line left its old pixels behind, "papered over with garbage"
 *   - the editing cursor left an underscore at every column it passed through,
 *     which read as an underline under the whole line being typed
 *
 * These assertions are about *pixels*, deliberately. Every other sink test
 * checks the character grid, and the grid was never the problem.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_render_erases_what_it_drew(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    akbasic_AkglSink *sink = NULL;
    int row = 0;

    PASS(errctx, start_frontend(NULL));
    sink = &FRONTEND.akglstate;

    /* Something on screen to erase. */
    PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "XYZ"));
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");
    TEST_REQUIRE(!cell_is_blank(shot, sink, 0, 0), "XYZ should have been drawn");
    SDL_DestroySurface(shot);

    /*
     * A clear must reach the glass, not just the grid. This is SCNCLR, and it is
     * the simplest shape of the bug: the grid empties and the pixels stay.
     */
    PASS(errctx, FRONTEND.akglsink.clear(&FRONTEND.akglsink));
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");
    for ( row = 0; row < 3; row++ ) {
	TEST_REQUIRE(cell_is_blank(shot, sink, 0, row),
		     "row %d still shows its old pixels after a clear", row);
    }
    SDL_DestroySurface(shot);

    /* A line that gets *shorter* erases the tail it no longer occupies. */
    PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "ABCD"));
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
    PASS(errctx, FRONTEND.akglsink.clear(&FRONTEND.akglsink));
    PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "AB"));
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");
    TEST_REQUIRE(!cell_is_blank(shot, sink, 0, 0), "AB should still be drawn");
    TEST_REQUIRE(cell_is_blank(shot, sink, 2, 0),
		 "the third cell held a C and must now be blank");
    TEST_REQUIRE(cell_is_blank(shot, sink, 3, 0),
		 "the fourth cell held a D and must now be blank");
    SDL_DestroySurface(shot);

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Scrolling reaches the glass: the bottom row is blank after it scrolls.
 *
 * `scroll()` shifts the grid up and clears the last row, and that was always
 * correct. What was not correct was the screen: nothing erased the row's old
 * pixels, so the text appeared to be overwritten with garbage rather than
 * scrolled.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_scroll_reaches_the_screen(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    akbasic_AkglSink *sink = NULL;
    int i = 0;

    PASS(errctx, start_frontend(NULL));
    sink = &FRONTEND.akglstate;

    /*
     * Fill every row *and render the bottom one* before scrolling it away. The
     * first version of this test wrote all the rows in one loop, which scrolled
     * the bottom row off before it had ever been drawn -- so there were no stale
     * pixels to find and the test passed whether or not the bug was there. It is
     * only a test of the scroll if the row reaches the glass first.
     */
    for ( i = 0; i < sink->rows - 1; i++ ) {
	PASS(errctx, FRONTEND.akglsink.writeln(&FRONTEND.akglsink, "FULL"));
    }
    PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "BOTTOM"));
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));

    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");
    TEST_REQUIRE(!cell_is_blank(shot, sink, 0, sink->rows - 1),
		 "the bottom row should have been drawn before we scroll it");
    SDL_DestroySurface(shot);

    /* Now scroll it away. The grid clears the bottom row; the screen must too. */
    PASS(errctx, FRONTEND.akglsink.writeln(&FRONTEND.akglsink, ""));
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));

    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");
    TEST_REQUIRE_STR(sink->text[sink->rows - 1], "");
    TEST_REQUIRE(cell_is_blank(shot, sink, 0, sink->rows - 1),
		 "the bottom row scrolled away but its pixels are still on screen");
    SDL_DestroySurface(shot);

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Backspace erases across a line wrap, taking the cursor to the row above.
 *
 * Called out explicitly in the report: backspace has to clear the character and
 * step back "at all times, including when the backspace key would move the
 * cursor up to the previous line on the Y axis". A line long enough to wrap puts
 * the tail of the edit on the row below its anchor, and backspacing past the
 * wrap has to erase there and return the cursor to the row above.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_backspace_across_a_wrap(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    akbasic_AkglSink *sink = NULL;
    char line[256];
    bool eof = true;
    int over = 0;
    int i = 0;

    PASS(errctx, start_frontend(NULL));
    sink = &FRONTEND.akglstate;
    PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));

    /* Two characters past the right-hand edge, so the edit wraps onto row 1. */
    over = sink->columns + 2;
    for ( i = 0; i < over; i++ ) {
	push_text_key('x', "x");
    }
    push_key('\r');
    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE_INT((int)strlen(line), over);
    TEST_REQUIRE(sink->text[1][0] != '\0', "a line past the edge must wrap onto the next row");
    /*
     * Render it, so row 1 genuinely has pixels to leave behind. Without this the
     * pixel assertions below pass whether or not anything erases, because the
     * wrapped row would never have been drawn in the first place.
     */
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));

    /*
     * Now type the same over-long line again and backspace back past the wrap.
     * The editor is re-entered, so the anchor is row 2 by now; what matters is
     * that the wrapped tail is erased and the cursor comes back a row.
     */
    PASS(errctx, FRONTEND.akglsink.clear(&FRONTEND.akglsink));
    for ( i = 0; i < over; i++ ) {
	push_text_key('y', "y");
    }
    for ( i = 0; i < 4; i++ ) {
	push_key(SDLK_BACKSPACE);
    }
    push_key('\r');
    PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
    TEST_REQUIRE_INT((int)strlen(line), over - 4);
    /* Back inside the first row: the wrap is gone, so row 1 holds nothing. */
    TEST_REQUIRE_STR(sink->text[1], "");
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));

    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");
    TEST_REQUIRE(cell_is_blank(shot, sink, 0, 1),
		 "the wrapped tail was backspaced away but is still on screen");
    /* And the last surviving character is where it should be, on row 0. */
    TEST_REQUIRE(!cell_is_blank(shot, sink, over - 5, 0),
		 "the character before the backspaces should still be drawn");
    TEST_REQUIRE(cell_is_blank(shot, sink, over - 4, 0),
		 "the first backspaced character should be gone");
    SDL_DestroySurface(shot);

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Nothing is drawn under the text being typed.
 *
 * The editing cursor used to be an underscore glyph drawn at the cursor cell.
 * With nothing erasing the previous frame it accumulated at every column the
 * cursor had passed through, which read as an underline beneath the whole line
 * and made typed text hard to read. Removed outright; §5 records it.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_block_cursor_blinks(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    akbasic_AkglSink *sink = NULL;
    int col = 0;
    int y = 0;
    int lit = 0;

    PASS(errctx, start_frontend(NULL));
    sink = &FRONTEND.akglstate;

    PASS(errctx, FRONTEND.akglsink.write(&FRONTEND.akglsink, "ab"));
    sink->editing = true;

    /* Period zero holds it solid, so a frame is deterministic. */
    sink->cursorperiodms = 0;
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");

    /*
     * A *block*: every scanline of the cursor cell is lit, including the top
     * one. An underscore lights only the bottom, and it was the underscore
     * sitting under the text that made typed text hard to read.
     */
    TEST_REQUIRE(anything_drawn(shot, sink->x + (2 * sink->cellw), sink->y, sink->cellw, 1),
		 "the cursor cell's top scanline should be lit: a block, not an underscore");
    TEST_REQUIRE(anything_drawn(shot, sink->x + (2 * sink->cellw),
				sink->y + sink->cellh - 1, sink->cellw, 1),
		 "the cursor cell's bottom scanline should be lit too");

    /* And nothing under the text itself, which is what was reported. */
    y = sink->y + sink->cellh - 1;
    for ( col = 0; col < 2; col++ ) {
	if ( anything_drawn(shot, sink->x + (col * sink->cellw), y, sink->cellw, 1) ) {
	    lit += 1;
	}
    }
    TEST_REQUIRE_INT(lit, 0);
    SDL_DestroySurface(shot);

    /*
     * It blinks. A short period and a wait past half of it: the same cell must
     * come out differently, which is the whole of what blinking is.
     */
    sink->cursorperiodms = 200;
    while ( (SDL_GetTicks() % 200) >= 100 ) {
	SDL_Delay(5);
    }
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(anything_drawn(shot, sink->x + (2 * sink->cellw), sink->y,
				sink->cellw, sink->cellh),
		 "the cursor should be visible in the first half of its period");
    SDL_DestroySurface(shot);

    while ( (SDL_GetTicks() % 200) < 100 ) {
	SDL_Delay(5);
    }
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(!anything_drawn(shot, sink->x + (2 * sink->cellw), sink->y,
				 sink->cellw, sink->cellh),
		 "the cursor should be hidden in the second half of its period");
    SDL_DestroySurface(shot);

    sink->editing = false;
    sink->cursorperiodms = 0;
    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A frame owns every pixel it presents, including ones it did not write.
 *
 * **This is the regression test for the blinking backspace, and it is the only
 * one that could have caught it without real hardware.** The sink used to skip
 * rows it believed were already clear, tracked in a `drawn[]` array. That
 * assumes a frame inherits the frame before it. SDL_RenderPresent swaps
 * buffers, so a frame inherits the one *two* back -- a row erased once is clean
 * in one buffer and dirty in the other, and presenting alternates between them.
 * The first character of a backspaced wrap flickered forever.
 *
 * It reproduced on X11 and never under the dummy driver, because a software
 * renderer with a single target does preserve. Rather than require real
 * hardware, this paints the stale pixels by hand -- which is exactly what the
 * other buffer hands back -- and asserts the sink erases them anyway.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_frame_owns_every_pixel(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    akbasic_AkglSink *sink = NULL;
    SDL_FRect stale;

    PASS(errctx, start_frontend(NULL));
    sink = &FRONTEND.akglstate;

    /* One line of text, and the grid otherwise empty. */
    PASS(errctx, FRONTEND.akglsink.writeln(&FRONTEND.akglsink, "KEPT"));
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));

    /*
     * Now dirty row 1 behind the sink's back, the way a swapped-in buffer
     * arrives already dirty. The sink has never drawn there and its grid says
     * the row is empty, so nothing in its own bookkeeping suggests any work.
     */
    stale.x = (float)sink->x;
    stale.y = (float)(sink->y + sink->cellh);
    stale.w = (float)sink->cellw;
    stale.h = (float)sink->cellh;
    PASS(errctx, akgl_draw_filled_rect(FRONTEND.renderer, &stale, sink->color));

    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(!cell_is_blank(shot, sink, 0, 1), "the stale pixels should be there to start");
    SDL_DestroySurface(shot);

    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");
    TEST_REQUIRE(cell_is_blank(shot, sink, 0, 1),
		 "a row the grid says is empty must be erased even if this sink never drew it");
    TEST_REQUIRE(!cell_is_blank(shot, sink, 0, 0), "and the row that has text is still drawn");
    SDL_DestroySurface(shot);

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A whole REPL session typed at the window, ending with QUIT.
 *
 * The end-to-end case: keystrokes into SDL's queue, out through the editor, into
 * the interpreter, and the result on both output paths. Nothing here is mocked
 * except the keyboard and the mirror.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_repl_session(void)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, start_frontend(NULL));
    PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));

    /*
     * Every keystroke queued up front, and the session ended with a typed QUIT
     * rather than a close event: the pump drains the whole SDL queue in one
     * call, so a close event queued here would be seen before the first
     * keystroke was ever read.
     */
    push_line("10 print 1");
    push_line("run");
    push_line("quit");

    PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_REPL));
    PASS(errctx, akbasic_frontend_akgl_drive(&FRONTEND, &RUNTIME));

    TEST_REQUIRE_INT(RUNTIME.mode, AKBASIC_MODE_QUIT);
    /* The reference prints READY at the prompt and after a RUN. */
    TEST_REQUIRE_STR(OUTPUT, "READY\n1\nREADY\n");

    stop_frontend();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The frontend leaves libakgl able to draw an actor, which is what a sprite is.
 *
 * Three things the host owes libakgl and that akgl_game_init() would normally
 * have done -- the object pools, the name registries, and the camera. Nothing in
 * the interpreter needed any of them until sprites, because text and the drawing
 * primitives go straight at the renderer; an actor goes through all three.
 *
 * Asserted by building one actor by hand and drawing it, rather than by reading
 * the globals back. Without akgl_registry_init() the actor cannot be named and
 * akgl_actor_initialize raises AKERR_KEY; without akgl_heap_init() there is no
 * pool to take it from; without a camera akgl_actor_render() dereferences NULL
 * and the test does not report a failure so much as stop existing.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_host_can_draw_an_actor(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *pattern = NULL;
    SDL_Surface *shot = NULL;
    SDL_Texture *texture = NULL;
    akgl_SpriteSheet *sheet = NULL;
    akgl_Sprite *sprite = NULL;
    akgl_Character *basechar = NULL;
    akgl_Actor *actor = NULL;
    /*
     * 128 bytes each, not string literals: akgl_sprite_initialize() and
     * akgl_actor_initialize() memcpy a fixed AKGL_*_MAX_NAME_LENGTH from what
     * they are handed, so a shorter buffer is read past its end.
     */
    char sheetname[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
    char spritename[AKGL_SPRITE_MAX_NAME_LENGTH];
    char charname[AKGL_SPRITE_MAX_NAME_LENGTH];
    char actorname[AKGL_ACTOR_MAX_NAME_LENGTH];

    PASS(errctx, start_frontend(NULL));

    TEST_REQUIRE(akgl_camera != NULL, "the frontend should have pointed the camera somewhere");
    TEST_REQUIRE_INT((int)akgl_camera->w, WINDOW_W);
    TEST_REQUIRE_INT((int)akgl_camera->h, WINDOW_H);
    TEST_REQUIRE(AKGL_REGISTRY_ACTOR != 0, "the frontend should have created the registries");

    memset(sheetname, 0, sizeof(sheetname));
    memset(spritename, 0, sizeof(spritename));
    memset(charname, 0, sizeof(charname));
    memset(actorname, 0, sizeof(actorname));
    SDL_strlcpy(sheetname, "akbasic:test:sheet", sizeof(sheetname));
    SDL_strlcpy(spritename, "akbasic:test:sprite", sizeof(spritename));
    SDL_strlcpy(charname, "akbasic:test:character", sizeof(charname));
    SDL_strlcpy(actorname, "akbasic:test:actor", sizeof(actorname));

    /* A solid 8x8 white square, built in memory rather than loaded from a file. */
    pattern = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32);
    TEST_REQUIRE(pattern != NULL, "could not create the pattern surface: %s", SDL_GetError());
    TEST_REQUIRE(SDL_FillSurfaceRect(pattern, NULL,
				     SDL_MapSurfaceRGBA(pattern, 0xff, 0xff, 0xff, 0xff)),
		 "could not fill the pattern surface: %s", SDL_GetError());
    texture = SDL_CreateTextureFromSurface(FRONTEND.renderer->sdl_renderer, pattern);
    SDL_DestroySurface(pattern);
    TEST_REQUIRE(texture != NULL, "could not upload the pattern: %s", SDL_GetError());

    /*
     * The sheet is assembled rather than initialized: akgl_spritesheet_initialize
     * loads an image from a path, and there is no file here. Everything below it
     * is the ordinary API.
     */
    PASS(errctx, akgl_heap_next_spritesheet(&sheet));
    memset(sheet, 0, sizeof(*sheet));
    sheet->texture = texture;
    SDL_strlcpy(sheet->name, sheetname, sizeof(sheet->name));
    sheet->refcount = 1;

    PASS(errctx, akgl_heap_next_sprite(&sprite));
    PASS(errctx, akgl_sprite_initialize(sprite, spritename, sheet));
    sprite->frames = 1;
    sprite->frameids[0] = 0;
    sprite->width = 8;
    sprite->height = 8;

    PASS(errctx, akgl_heap_next_character(&basechar));
    PASS(errctx, akgl_character_initialize(basechar, charname));
    PASS(errctx, akgl_character_sprite_add(basechar, sprite, 0));

    PASS(errctx, akgl_heap_next_actor(&actor));
    PASS(errctx, akgl_actor_initialize(actor, actorname));
    PASS(errctx, akgl_actor_set_character(actor, charname));
    actor->state = 0;
    actor->visible = true;
    actor->x = 40.0f;
    actor->y = 24.0f;

    /* The text layer paints every cell it owns, so draw the actor over it. */
    PASS(errctx, akbasic_sink_akgl_render(&FRONTEND.akglsink));
    PASS(errctx, akgl_actor_render(actor));

    shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
    TEST_REQUIRE(shot != NULL, "could not read the render target back");
    TEST_REQUIRE(anything_drawn(shot, 42, 26, 2, 2),
		 "the actor should have been drawn at its own coordinates");
    SDL_DestroySurface(shot);

    stop_frontend();

    /*
     * And the pools start over. akgl_heap_init() is not needed to make the
     * *first* frontend work -- the pool arrays are file-scope and start zeroed --
     * so the only thing that shows it ran is a second frontend handing back the
     * slot the first one was still holding. Without it the actor above survives
     * SDL_Quit with a live refcount, pointing at a texture that no longer exists,
     * and the next program's sprites are allocated around the corpse.
     */
    {
	akgl_Actor *reused = NULL;

	PASS(errctx, start_frontend(NULL));
	PASS(errctx, akgl_heap_next_actor(&reused));
	TEST_REQUIRE(reused == actor,
		     "a new frontend should hand back the released pool slot, not the one after it");
	stop_frontend();
    }
    SUCCEED_RETURN(errctx);
}

/** @brief Every entry point validates its pointers before touching SDL. */
static akerr_ErrorContext AKERR_NOIGNORE *test_arguments_refused(void)
{
    PREPARE_ERROR(errctx);
    bool running = false;

    TEST_REQUIRE_STATUS(akbasic_frontend_akgl_init(NULL, "t", WINDOW_W, WINDOW_H,
						   AKBASIC_TEST_C64_FONT, 16, NULL, NULL),
			AKERR_NULLPOINTER);
    TEST_REQUIRE_STATUS(akbasic_frontend_akgl_init(&FRONTEND, "t", WINDOW_W, WINDOW_H,
						   NULL, 16, NULL, NULL),
			AKERR_NULLPOINTER);
    /* A zero-point font would divide by zero measuring the character grid. */
    TEST_REQUIRE_STATUS(akbasic_frontend_akgl_init(&FRONTEND, "t", WINDOW_W, WINDOW_H,
						   AKBASIC_TEST_C64_FONT, 0, NULL, NULL),
			AKBASIC_ERR_VALUE);
    TEST_REQUIRE_STATUS(akbasic_frontend_akgl_attach(NULL, &RUNTIME), AKERR_NULLPOINTER);
    TEST_REQUIRE_STATUS(akbasic_frontend_akgl_pump(NULL, &running), AKERR_NULLPOINTER);
    TEST_REQUIRE_STATUS(akbasic_frontend_akgl_drive(&FRONTEND, NULL), AKERR_NULLPOINTER);
    /* Shutting down something that was never brought up is not an error. */
    akbasic_frontend_akgl_shutdown(NULL);
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    PREPARE_ERROR(errctx);

    SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
    SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
    SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");

    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	CATCH(errctx, akbasic_error_register());

	CATCH(errctx, test_arguments_refused());
	CATCH(errctx, test_program_reaches_both());
	CATCH(errctx, test_pump_feeds_input());
	CATCH(errctx, test_window_close_stops());
	CATCH(errctx, test_line_editor());
	CATCH(errctx, test_text_input_is_started());
	CATCH(errctx, test_render_erases_what_it_drew());
	CATCH(errctx, test_scroll_reaches_the_screen());
	CATCH(errctx, test_backspace_across_a_wrap());
	CATCH(errctx, test_block_cursor_blinks());
	CATCH(errctx, test_frame_owns_every_pixel());
	CATCH(errctx, test_repl_session());
	CATCH(errctx, test_host_can_draw_an_actor());
    } CLEANUP {
	stop_frontend();
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "akgl frontend test failed");
	akbasic_test_failures += 1;
	/*
	 * FINISH_NORETURN rather than FINISH, matching tests/akgl_backends.c:
	 * FINISH expands a `return __err_context` that this int-returning
	 * function cannot compile even where the branch is unreachable.
	 * HANDLE_DEFAULT has already marked the context handled, so nothing
	 * aborts.
	 */
    } FINISH_NORETURN(errctx);

    return akbasic_test_failures;
}
