/**
 * @file ui.c
 * @brief Unit tests for the UI subsystem: lifecycle, fonts, measurement and rendering.
 *
 * The lifecycle and arena tests run without a renderer -- akgl_ui_init takes
 * its layout dimensions as parameters precisely so that a headless program
 * can bring the subsystem up. The layout tests draw real clay layouts into a
 * small software renderer under the dummy video driver and read the target
 * back, the same arrangement tests/draw.c uses, so their assertions are about
 * pixels rather than about clay having been called.
 */

#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>

#include <akgl/draw.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/text.h>
#include <akgl/ui.h>

#include "testutil.h"

/** @brief Width and height of the offscreen target the layout tests draw into. */
#define TEST_TARGET_SIZE 64

/** @brief The font every text test measures and draws with. */
#define TEST_FONT_PATH "assets/akgl_test_mono.ttf"
/** @brief Registry name the test font is loaded under. */
#define TEST_FONT_NAME "uifont"
/** @brief Point size the test font is loaded at. */
#define TEST_FONT_SIZE 16

/** @brief Opaque black, what each layout test clears the target to. */
static const SDL_Color testblack = { 0x00, 0x00, 0x00, 0xff };
/** @brief The color most layout tests fill with. */
static const SDL_Color testred = { 0xff, 0x00, 0x00, 0xff };
/** @brief A second color, for borders against fills. */
static const SDL_Color testgreen = { 0x00, 0xff, 0x00, 0xff };

/** @brief Clear the whole target to opaque black. */
static akerr_ErrorContext *clear_target(void)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    FAIL_ZERO_RETURN(
	errctx,
	SDL_RenderClear(akgl_renderer->sdl_renderer),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}

/** @brief Report whether one pixel of @p shot carries @p color. Alpha is not compared. */
static bool pixel_is(SDL_Surface *shot, int x, int y, SDL_Color color)
{
    uint8_t r = 0;
    uint8_t g = 0;
    uint8_t b = 0;
    uint8_t a = 0;

    if ( shot == NULL ) {
	return false;
    }
    if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) {
	return false;
    }
    return ((r == color.r) && (g == color.g) && (b == color.b));
}

/**
 * @brief Init must refuse bad dimensions and an uninitialized resize.
 *
 * Runs before anything initializes the subsystem, because the "not
 * initialized" refusal is half of what it asserts.
 */
akerr_ErrorContext *test_ui_validation(void)
{
    PREPARE_ERROR(e);

    ATTEMPT {
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_ui_init(0, 240),
			   "akgl_ui_init accepted a zero width");
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_ui_init(320, -1),
			   "akgl_ui_init accepted a negative height");
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_resize(320, 240),
			   "akgl_ui_resize worked on an uninitialized subsystem");
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_begin(),
			   "akgl_ui_frame_begin worked on an uninitialized subsystem");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief Init, double-init refusal, idempotent shutdown, re-init.
 *
 * The double init has to be refused rather than absorbed: a second
 * Clay_Initialize over a live context would silently discard every element id
 * and scroll position the first one held, which is the kind of reset a game
 * only notices as "the menu forgot where it was".
 */
akerr_ErrorContext *test_ui_lifecycle(void)
{
    PREPARE_ERROR(e);

    ATTEMPT {
	TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "first akgl_ui_init failed");
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_init(320, 240),
			   "a second akgl_ui_init was not refused");
	TEST_EXPECT_OK(e, akgl_ui_resize(640, 480), "resize on a live subsystem failed");
	TEST_EXPECT_OK(e, akgl_ui_shutdown(), "akgl_ui_shutdown failed");
	TEST_EXPECT_OK(e, akgl_ui_shutdown(), "a second akgl_ui_shutdown was not a no-op");
	TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "re-init after shutdown failed");
	TEST_EXPECT_OK(e, akgl_ui_shutdown(), "shutdown after re-init failed");
    } CLEANUP {
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief A too-small arena must refuse at init, loudly, and recover.
 *
 * The narrowed limit stands in for a consumer who raised AKGL_UI_MAX_ELEMENTS
 * without raising AKGL_UI_ARENA_BYTES: the refusal at startup, with both
 * numbers in the message, is the whole safety story -- past init, clay trusts
 * the arena completely. Same shape as the collision arena's exhaustion test,
 * for the same reason: rebuilding with a smaller ceiling is not something CI
 * can do.
 */
akerr_ErrorContext *test_ui_arena_exhaustion(void)
{
    PREPARE_ERROR(e);

    ATTEMPT {
	akgl_ui_arena_limit(64);
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_init(320, 240),
			   "akgl_ui_init accepted an arena clay cannot fit in");
	akgl_ui_arena_limit(0);
	TEST_EXPECT_OK(e, akgl_ui_init(320, 240),
		       "akgl_ui_init failed after the arena limit was restored");
	TEST_EXPECT_OK(e, akgl_ui_shutdown(), "shutdown after the exhaustion test failed");
    } CLEANUP {
	akgl_ui_arena_limit(0);
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief The fontId table: issue, dedupe, refuse the unknown and the overfull.
 *
 * The table stores names and resolves them per use, so the interesting cases
 * are all at registration: an id for a loaded font, the same id again for the
 * same name, a refusal for a name nothing loaded, and a refusal -- naming the
 * ceiling -- when the table is full.
 */
akerr_ErrorContext *test_ui_font_register(void)
{
    PREPARE_ERROR(e);
    uint16_t fontid = 99;
    uint16_t again = 99;
    uint16_t extra = 99;
    char slotname[8];
    bool loaded = true;
    bool registered = true;
    int count = 0;
    int i = 0;

    ATTEMPT {
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
			   "font registration worked on an uninitialized subsystem");
	TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "init for the font table test failed");

	TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
		       "registering a loaded font failed");
	TEST_ASSERT(e, fontid == 0, "the first fontId issued was %u, not 0", fontid);
	TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &again),
		       "re-registering the same name failed");
	TEST_ASSERT(e, again == fontid,
		    "re-registering the same name issued a second id (%u after %u)", again, fontid);

	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_font_register("nothing-loaded-this", &extra),
			   "a name absent from the font registry was registered");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_font_register(NULL, &extra),
			   "a NULL name was accepted");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_font_register(TEST_FONT_NAME, NULL),
			   "a NULL id destination was accepted");

	// Fill the table: the same face under fresh names costs a slot each,
	// because the size-baked handle is what a slot maps to.
	for ( i = 1; i < AKGL_UI_MAX_FONTS; i++ ) {
	    IGNORE(aksl_snprintf(&count, slotname, sizeof(slotname), "f%d", i));
	    TEST_ASSERT_FLAG(loaded, akgl_text_loadfont(slotname, TEST_FONT_PATH, TEST_FONT_SIZE) == NULL);
	    TEST_ASSERT_FLAG(registered, akgl_ui_font_register(slotname, &extra) == NULL);
	}
	TEST_ASSERT(e, loaded, "loading the fill fonts failed");
	TEST_ASSERT(e, registered, "registering the fill fonts failed");
	TEST_EXPECT_OK(e, akgl_text_loadfont("overflow", TEST_FONT_PATH, TEST_FONT_SIZE),
		       "loading the overflow font failed");
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_font_register("overflow", &extra),
			   "a full fontId table issued another id");
    } CLEANUP {
	IGNORE(akgl_text_unloadfont("overflow"));
	for ( i = 1; i < AKGL_UI_MAX_FONTS; i++ ) {
	    IGNORE(aksl_snprintf(&count, slotname, sizeof(slotname), "f%d", i));
	    IGNORE(akgl_text_unloadfont(slotname));
	}
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief The measure bridge must agree with akgl_text_measure, slice or not.
 *
 * clay hands the callback slices carved out of larger strings, so the case
 * that matters is a slice whose stated length stops short of the bytes behind
 * it -- five bytes of "HelloWorld" must measure exactly as "Hello" does. A
 * fontId nothing issued reports zero-by-zero, the only failure channel the
 * callback signature allows.
 */
akerr_ErrorContext *test_ui_measure_text(void)
{
    PREPARE_ERROR(e);
    Clay_TextElementConfig config;
    Clay_StringSlice slice;
    Clay_Dimensions sliced;
    Clay_Dimensions whole;
    TTF_Font *font = NULL;
    uint16_t fontid = 0;
    int w = 0;
    int h = 0;

    ATTEMPT {
	TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "init for the measure test failed");
	TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
		       "registering the measure test font failed");
	font = (TTF_Font *)SDL_GetPointerProperty(AKGL_REGISTRY_FONT, TEST_FONT_NAME, NULL);
	FAIL_ZERO_BREAK(e, font, AKGL_ERR_BEHAVIOR, "the test font left the registry");

	SDL_memset(&config, 0x00, sizeof(config));
	config.fontId = fontid;

	slice.chars = "HelloWorld";
	slice.baseChars = slice.chars;
	slice.length = 5;
	sliced = akgl_ui_measure_text(slice, &config, NULL);
	TEST_EXPECT_OK(e, akgl_text_measure(font, "Hello", &w, &h),
		       "measuring the whole string through the text subsystem failed");
	TEST_ASSERT_FEQ(e, sliced.width, (float)w,
			"a 5-byte slice of HelloWorld measured %f wide; Hello measures %d",
			(double)sliced.width, w);
	TEST_ASSERT_FEQ(e, sliced.height, (float)h,
			"a 5-byte slice of HelloWorld measured %f high; Hello measures %d",
			(double)sliced.height, h);

	slice.length = 10;
	whole = akgl_ui_measure_text(slice, &config, NULL);
	TEST_ASSERT(e, whole.width > sliced.width,
		    "HelloWorld did not measure wider than its Hello prefix");

	config.fontId = (uint16_t)(AKGL_UI_MAX_FONTS + 1);
	sliced = akgl_ui_measure_text(slice, &config, NULL);
	TEST_ASSERT_FEQ(e, sliced.width, 0.0f,
			"an unregistered fontId measured %f wide instead of 0",
			(double)sliced.width);
	TEST_ASSERT_FEQ(e, sliced.height, 0.0f,
			"an unregistered fontId measured %f high instead of 0",
			(double)sliced.height);
    } CLEANUP {
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief The frame bracket refuses to nest, to close unopened, and recovers from failure.
 */
akerr_ErrorContext *test_ui_frame_bracket(void)
{
    PREPARE_ERROR(e);

    ATTEMPT {
	TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
		       "init for the frame bracket test failed");
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_end(akgl_renderer),
			   "frame_end closed a frame nothing opened");
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening a frame failed");
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_begin(),
			   "a second frame_begin was not refused");
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer),
		       "closing an empty frame failed");
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_end(akgl_renderer),
			   "frame_end closed the same frame twice");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_frame_end(NULL),
			   "frame_end accepted a NULL backend");
    } CLEANUP {
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief One real layout, executed, read back: fill, border and clip land where clay put them.
 *
 * An outer container with 8 pixels of padding and a fixed 20x20 child pins
 * the child's box to (8,8)-(28,28) without depending on any layout behaviour
 * subtler than "padding is padding".
 */
akerr_ErrorContext *test_ui_layout_pixels(void)
{
    PREPARE_ERROR(e);
    SDL_Surface *shot = NULL;

    ATTEMPT {
	TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
		       "init for the layout test failed");

	// A filled rectangle.
	CATCH(e, clear_target());
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the fill frame failed");
	CLAY({ .layout = { .padding = { 8, 0, 8, 0 } } }) {
	    CLAY({
		    .layout = {
			.sizing = {
			    .width = CLAY_SIZING_FIXED(20),
			    .height = CLAY_SIZING_FIXED(20)
			}
		    },
		    .backgroundColor = { 255, 0, 0, 255 }
		}) {}
	}
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the fill frame failed");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(e, pixel_is(shot, 10, 10, testred),
		    "the fill did not land inside the child's box");
	TEST_ASSERT(e, pixel_is(shot, 27, 27, testred),
		    "the fill stopped short of the child's far corner");
	TEST_ASSERT(e, pixel_is(shot, 7, 7, testblack),
		    "the fill leaked outside the child's box");
	TEST_ASSERT(e, pixel_is(shot, 40, 40, testblack),
		    "the fill reached pixels no element occupies");
	SDL_DestroySurface(shot);
	shot = NULL;

	// A border: green edges two pixels wide, nothing in the middle.
	CATCH(e, clear_target());
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the border frame failed");
	CLAY({ .layout = { .padding = { 8, 0, 8, 0 } } }) {
	    CLAY({
		    .layout = {
			.sizing = {
			    .width = CLAY_SIZING_FIXED(20),
			    .height = CLAY_SIZING_FIXED(20)
			}
		    },
		    .border = {
			.color = { 0, 255, 0, 255 },
			.width = { 2, 2, 2, 2, 0 }
		    }
		}) {}
	}
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the border frame failed");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(e, pixel_is(shot, 18, 9, testgreen), "the top border edge is missing");
	TEST_ASSERT(e, pixel_is(shot, 18, 26, testgreen), "the bottom border edge is missing");
	TEST_ASSERT(e, pixel_is(shot, 9, 18, testgreen), "the left border edge is missing");
	TEST_ASSERT(e, pixel_is(shot, 26, 18, testgreen), "the right border edge is missing");
	TEST_ASSERT(e, pixel_is(shot, 18, 18, testblack), "the border filled its middle");
	SDL_DestroySurface(shot);
	shot = NULL;

	// A clipped child: a 30x30 fill inside a 10x10 clipping container
	// must stop at the container's edge.
	CATCH(e, clear_target());
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the clip frame failed");
	CLAY({ .layout = { .padding = { 8, 0, 8, 0 } } }) {
	    CLAY({
		    .layout = {
			.sizing = {
			    .width = CLAY_SIZING_FIXED(10),
			    .height = CLAY_SIZING_FIXED(10)
			}
		    },
		    .clip = { .horizontal = true, .vertical = true }
		}) {
		CLAY({
			.layout = {
			    .sizing = {
				.width = CLAY_SIZING_FIXED(30),
				.height = CLAY_SIZING_FIXED(30)
			    }
			},
			.backgroundColor = { 255, 0, 0, 255 }
		    }) {}
	    }
	}
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the clip frame failed");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(e, pixel_is(shot, 10, 10, testred),
		    "the clipped child did not draw inside the clip");
	TEST_ASSERT(e, pixel_is(shot, 25, 25, testblack),
		    "the child escaped its clipping container");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief Text laid out by clay lands on the target, and in its colour.
 *
 * Which pixels a rasterized glyph occupies is SDL_ttf's business, so the
 * assertion is that *some* pixel in the text's box carries the text colour --
 * enough to prove the measure bridge, the scratch copy and the draw all met.
 */
akerr_ErrorContext *test_ui_layout_text(void)
{
    PREPARE_ERROR(e);
    SDL_Surface *shot = NULL;
    uint16_t fontid = 0;
    bool found = false;
    int x = 0;
    int y = 0;

    ATTEMPT {
	TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
		       "init for the text layout test failed");
	TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
		       "registering the layout test font failed");

	CATCH(e, clear_target());
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the text frame failed");
	CLAY({ .layout = { .padding = { 4, 0, 4, 0 } } }) {
	    CLAY_TEXT(CLAY_STRING("Hi"), CLAY_TEXT_CONFIG({
			.fontId = fontid,
			.textColor = { 255, 0, 0, 255 }
		    }));
	}
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the text frame failed");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	for ( y = 0; y < (TEST_TARGET_SIZE / 2); y++ ) {
	    for ( x = 0; x < (TEST_TARGET_SIZE / 2); x++ ) {
		if ( pixel_is(shot, x, y, testred) ) {
		    found = true;
		}
	    }
	}
	TEST_ASSERT(e, found, "no pixel of the laid-out text carries the text colour");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/** @brief Build a left-button mouse event at a position, down or up. */
static void make_button_event(SDL_Event *event, uint32_t type, float x, float y)
{
    SDL_memset(event, 0x00, sizeof(SDL_Event));
    event->type = type;
    event->button.button = SDL_BUTTON_LEFT;
    event->button.x = x;
    event->button.y = y;
}

/**
 * @brief The consumed contract: clicks on the UI are the UI's, everything else passes.
 *
 * The hit test runs against the layout the previous frame retained, so the
 * test lays one panel out, then throws events at it: a click inside is
 * consumed, a click outside is not, motion never is, and -- the case most
 * likely to surprise -- a click before any layout exists is not consumed,
 * because there is nothing to be over.
 */
akerr_ErrorContext *test_ui_handle_event(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;
    bool consumed = true;
    int token = 1;

    ATTEMPT {
	// Uninitialized: everything passes through untouched.
	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "an event before init was refused");
	TEST_ASSERT(e, consumed == false, "an event before init was consumed");

	TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
		       "init for the event test failed");

	// Initialized but nothing laid out yet: a click lands on no tree.
	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "a click before any layout was refused");
	TEST_ASSERT(e, consumed == false, "a click was consumed before any layout existed");
	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_UP, 10.0f, 10.0f);
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "the release before any layout was refused");

	// Lay out one 20x20 panel at (8,8) for the hit tests that follow.
	CATCH(e, clear_target());
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the hit-test frame failed");
	CLAY({ .layout = { .padding = { 8, 0, 8, 0 } } }) {
	    CLAY({
		    .layout = {
			.sizing = {
			    .width = CLAY_SIZING_FIXED(20),
			    .height = CLAY_SIZING_FIXED(20)
			}
		    },
		    .backgroundColor = { 255, 0, 0, 255 }
		}) {}
	}
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the hit-test frame failed");

	// A press on the panel is the UI's; its release is too.
	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "the press on the panel was refused");
	TEST_ASSERT(e, consumed == true, "a press on a UI panel was not consumed");
	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_UP, 10.0f, 10.0f);
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "the release on the panel was refused");
	TEST_ASSERT(e, consumed == true, "a release on a UI panel was not consumed");

	// A press beside the panel belongs to the game.
	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 50.0f, 50.0f);
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "the press beside the panel was refused");
	TEST_ASSERT(e, consumed == false, "a press beside the panel was consumed");
	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_UP, 50.0f, 50.0f);
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "the release beside the panel was refused");

	// A right-button press is not the UI's business at all.
	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
	event.button.button = SDL_BUTTON_RIGHT;
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "a right-button press was refused");
	TEST_ASSERT(e, consumed == false, "a right-button press was consumed");

	// Motion is never consumed, wherever it lands.
	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_MOUSE_MOTION;
	event.motion.x = 10.0f;
	event.motion.y = 10.0f;
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "motion over the panel was refused");
	TEST_ASSERT(e, consumed == false, "motion over a panel was consumed");

	// The wheel goes to the UI only while the pointer is over it. The
	// motion event above left the pointer on the panel.
	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_MOUSE_WHEEL;
	event.wheel.y = 1.0f;
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "the wheel over the panel was refused");
	TEST_ASSERT(e, consumed == true, "the wheel over a panel was not consumed");
	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_MOUSE_MOTION;
	event.motion.x = 50.0f;
	event.motion.y = 50.0f;
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "motion off the panel was refused");
	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_MOUSE_WHEEL;
	event.wheel.y = 1.0f;
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "the wheel beside the panel was refused");
	TEST_ASSERT(e, consumed == false, "the wheel beside the panel was consumed");

	// A resize reaches the layout engine and still passes through.
	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_WINDOW_RESIZED;
	event.window.data1 = TEST_TARGET_SIZE;
	event.window.data2 = TEST_TARGET_SIZE;
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "a resize event was refused");
	TEST_ASSERT(e, consumed == false, "a resize event was consumed");

	// A key event is not the UI's; keyboard focus is the application's.
	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_KEY_DOWN;
	event.key.key = SDLK_RETURN;
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "a key event was refused");
	TEST_ASSERT(e, consumed == false, "a key event was consumed");

	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_handle_event(NULL, &event, &consumed),
			   "a NULL appstate was accepted");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_handle_event(&token, NULL, &consumed),
			   "a NULL event was accepted");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_handle_event(&token, &event, NULL),
			   "a NULL consumed destination was accepted");
    } CLEANUP {
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/** @brief The default widget palette's fill, for pixel assertions. Matches src/ui.c. */
static const SDL_Color widgetfill = { 24, 20, 37, 235 };
/** @brief The default widget palette's edge and ink. */
static const SDL_Color widgetink = { 240, 236, 214, 255 };

/**
 * @brief The dialog widget draws the textbox: fill inside, border on the edge, text on top.
 *
 * On a 64x64 layout with the default 8px padding the panel spans (8,0) to
 * (56,56) -- the width follows the layout, the height is
 * AKGL_UI_DIALOG_HEIGHT. The palette asserted here is the point: the default
 * style *is* the JRPG textbox's, so the comparison chapter can put the two
 * side by side.
 */
akerr_ErrorContext *test_ui_dialog_widget(void)
{
    PREPARE_ERROR(e);
    SDL_Surface *shot = NULL;
    uint16_t fontid = 0;

    ATTEMPT {
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_dialog("early", "words", NULL),
			   "a dialog was declared with no subsystem");
	TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
		       "init for the dialog test failed");
	TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
		       "registering the dialog test font failed");
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_dialog("early", "words", NULL),
			   "a dialog was declared outside the frame bracket");

	CATCH(e, clear_target());
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the dialog frame failed");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_dialog(NULL, "words", NULL),
			   "a NULL dialog id was accepted");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_dialog("dialog", NULL, NULL),
			   "NULL dialog text was accepted");
	TEST_EXPECT_OK(e, akgl_ui_dialog("dialog", "Hi", NULL), "declaring the dialog failed");
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the dialog frame failed");

	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(e, pixel_is(shot, 32, 40, widgetfill),
		    "the dialog panel's fill is missing from its middle");
	TEST_ASSERT(e, pixel_is(shot, 8, 28, widgetink),
		    "the dialog panel's border is missing from its left edge");
	TEST_ASSERT(e, pixel_is(shot, 4, 28, testblack),
		    "the dialog painted outside its margin");
	TEST_ASSERT(e, pixel_is(shot, 32, 60, testblack),
		    "the dialog painted over the margin below it");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief Labels pin to their corners and stay out of the others.
 */
akerr_ErrorContext *test_ui_label_widget(void)
{
    PREPARE_ERROR(e);
    SDL_Surface *shot = NULL;
    uint16_t fontid = 0;
    bool topright = false;
    bool bottomleft = false;
    int x = 0;
    int y = 0;

    ATTEMPT {
	TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
		       "init for the label test failed");
	TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
		       "registering the label test font failed");

	CATCH(e, clear_target());
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the label frame failed");
	TEST_EXPECT_OK(e, akgl_ui_label("score", "9", AKGL_UI_ANCHOR_TOP_RIGHT, NULL),
		       "declaring the top-right label failed");
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
			   akgl_ui_label("bogus", "9", (akgl_UiAnchor)99, NULL),
			   "a nonsense anchor was accepted");
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the label frame failed");

	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	// The label is fit-sized around its glyph, so where exactly it ends
	// is the font's business; the assertions are per-quadrant instead.
	for ( y = 0; y < (TEST_TARGET_SIZE / 2); y++ ) {
	    for ( x = (TEST_TARGET_SIZE / 2); x < TEST_TARGET_SIZE; x++ ) {
		if ( pixel_is(shot, x, y, widgetfill) ) {
		    topright = true;
		}
	    }
	}
	// The far corner, not the whole quadrant: a padded label on a 64px
	// target is fat enough to graze the quadrant boundary, and where its
	// edge falls is the font's business, not this test's.
	for ( y = (TEST_TARGET_SIZE - 16); y < TEST_TARGET_SIZE; y++ ) {
	    for ( x = 0; x < 16; x++ ) {
		if ( pixel_is(shot, x, y, widgetfill) ) {
		    bottomleft = true;
		}
	    }
	}
	TEST_ASSERT(e, topright, "the top-right label left no fill in its quadrant");
	TEST_ASSERT(e, bottomleft == false, "the top-right label reached the opposite corner");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief The menu widget: validation, the inverted highlight, and mouse selection.
 *
 * The mouse half runs the real two-frame protocol: frame one lays the menu
 * out, the click lands between frames against the retained tree, and frame
 * two's declaration sees the hover and the latched press edge. Row geometry
 * comes from Clay_GetElementData rather than being derived here, so the test
 * cannot drift from however the menu lays its rows out.
 */
akerr_ErrorContext *test_ui_menu_widget(void)
{
    PREPARE_ERROR(e);
    SDL_Surface *shot = NULL;
    SDL_Event event;
    Clay_ElementData rowdata;
    static akgl_UiMenu menu = {
	.id = "testmenu",
	.items = { "New Game", "Options", "Quit" },
	.count = 3,
    };
    akgl_UiMenu broken;
    uint16_t fontid = 0;
    bool consumed = false;
    bool inverted = false;
    int token = 1;
    int x = 0;
    int y = 0;

    ATTEMPT {
	TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
		       "init for the menu test failed");
	TEST_EXPECT_OK(e, akgl_ui_font_register(TEST_FONT_NAME, &fontid),
		       "registering the menu test font failed");

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_menu(NULL),
			   "a NULL menu was accepted");
	broken = menu;
	broken.count = 0;
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_ui_menu(&broken),
			   "an empty menu was accepted");
	broken = menu;
	broken.selected = 7;
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_ui_menu(&broken),
			   "a selection outside the menu was accepted");
	broken = menu;
	broken.items[1] = NULL;
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_menu(&broken),
			   "a NULL entry was accepted");

	// Frame one: the menu as declared, selection on the first row.
	CATCH(e, clear_target());
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the first menu frame failed");
	TEST_EXPECT_OK(e, akgl_ui_menu(&menu), "declaring the menu failed");
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the first menu frame failed");

	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(e, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	for ( y = 0; y < TEST_TARGET_SIZE; y++ ) {
	    for ( x = 0; x < TEST_TARGET_SIZE; x++ ) {
		if ( pixel_is(shot, x, y, widgetink) ) {
		    inverted = true;
		}
	    }
	}
	TEST_ASSERT(e, inverted, "no row carries the inverted highlight");

	// The click: aimed at the second row's real box, landed between
	// frames, seen by the declaration in frame two.
	rowdata = Clay_GetElementData(Clay_GetElementIdWithIndex(CLAY_STRING("testmenu"), 2));
	TEST_ASSERT(e, rowdata.found, "the second row's element id cannot be found");
	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_MOUSE_MOTION;
	event.motion.x = rowdata.boundingBox.x + (rowdata.boundingBox.width / 2.0f);
	event.motion.y = rowdata.boundingBox.y + (rowdata.boundingBox.height / 2.0f);
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "moving onto the second row was refused");
	make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN,
			  rowdata.boundingBox.x + (rowdata.boundingBox.width / 2.0f),
			  rowdata.boundingBox.y + (rowdata.boundingBox.height / 2.0f));
	TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
		       "the click on the second row was refused");
	TEST_ASSERT(e, consumed == true, "the click on the menu was not consumed");

	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the second menu frame failed");
	TEST_EXPECT_OK(e, akgl_ui_menu(&menu), "redeclaring the menu failed");
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the second menu frame failed");

	TEST_ASSERT(e, menu.selected == 1,
		    "clicking the second row moved the selection to %d, not 1", menu.selected);
	TEST_ASSERT(e, menu.activated == true, "clicking a row did not activate the menu");
	menu.activated = false;

	// The pointer is still parked over the second row. Keyboard moves the
	// selection away, and redeclaring the menu must not let the
	// stationary pointer claim it back -- only motion or a click selects.
	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_KEY_DOWN;
	event.key.key = SDLK_DOWN;
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed),
		       "Down after the click was refused");
	TEST_ASSERT(e, menu.selected == 2, "Down did not move the selection off the hovered row");
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the third menu frame failed");
	TEST_EXPECT_OK(e, akgl_ui_menu(&menu), "redeclaring the menu a third time failed");
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the third menu frame failed");
	TEST_ASSERT(e, menu.selected == 2,
		    "a stationary pointer stole the selection back to %d", menu.selected);
	TEST_ASSERT(e, menu.activated == false,
		    "a stationary pointer activated the menu");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief Keyboard and gamepad drive a menu: wrap both ways, confirm, ignore the rest.
 */
akerr_ErrorContext *test_ui_menu_events(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;
    static akgl_UiMenu menu = {
	.id = "eventmenu",
	.items = { "One", "Two", "Three" },
	.count = 3,
    };
    bool consumed = false;

    ATTEMPT {
	TEST_EXPECT_OK(e, akgl_ui_init(320, 240), "init for the menu event test failed");
	menu.selected = 0;
	menu.activated = false;

	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_KEY_DOWN;
	event.key.key = SDLK_DOWN;
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed), "Down was refused");
	TEST_ASSERT(e, consumed && (menu.selected == 1), "Down did not move the selection to 1");
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed), "Down was refused");
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed), "Down was refused");
	TEST_ASSERT(e, menu.selected == 0, "Down from the last row did not wrap to the first");
	event.key.key = SDLK_UP;
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed), "Up was refused");
	TEST_ASSERT(e, menu.selected == 2, "Up from the first row did not wrap to the last");
	event.key.key = SDLK_RETURN;
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed), "Return was refused");
	TEST_ASSERT(e, consumed && menu.activated, "Return did not activate the menu");
	menu.activated = false;
	event.key.key = SDLK_ESCAPE;
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed), "Escape was refused");
	TEST_ASSERT(e, consumed == false, "a key the menu does not use was consumed");

	SDL_memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
	event.gbutton.button = SDL_GAMEPAD_BUTTON_DPAD_DOWN;
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed), "D-pad down was refused");
	TEST_ASSERT(e, consumed && (menu.selected == 0), "D-pad down did not wrap to the first row");
	event.gbutton.button = SDL_GAMEPAD_BUTTON_SOUTH;
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed), "South was refused");
	TEST_ASSERT(e, consumed && menu.activated, "South did not activate the menu");
	menu.activated = false;
	event.gbutton.button = SDL_GAMEPAD_BUTTON_EAST;
	TEST_EXPECT_OK(e, akgl_ui_menu_handle_event(&menu, &event, &consumed), "East was refused");
	TEST_ASSERT(e, consumed == false, "a button the menu does not use was consumed");

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_ui_menu_handle_event(NULL, &event, &consumed),
			   "a NULL menu was accepted");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_ui_menu_handle_event(&menu, NULL, &consumed),
			   "a NULL event was accepted");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_ui_menu_handle_event(&menu, &event, NULL),
			   "a NULL consumed destination was accepted");
    } CLEANUP {
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/**
 * @brief A layout error inside clay must surface from frame_end, not vanish.
 *
 * The measure callback and clay's own error handler both report into a stash
 * with no other way out. Text declared against a fontId nothing issued is the
 * easiest such error to provoke, and the frame after the failed one must be
 * clean -- one bad frame, not a wedged subsystem.
 */
akerr_ErrorContext *test_ui_layout_error_surfaces(void)
{
    PREPARE_ERROR(e);

    ATTEMPT {
	TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
		       "init for the layout error test failed");
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the doomed frame failed");
	CLAY_TEXT(CLAY_STRING("doomed"), CLAY_TEXT_CONFIG({
		    .fontId = 7,
		    .textColor = { 255, 255, 255, 255 }
		}));
	TEST_EXPECT_STATUS(e, AKGL_ERR_UI, akgl_ui_frame_end(akgl_renderer),
			   "a layout with an unresolvable font closed cleanly");
	TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "the frame after a failed one was refused");
	TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer),
		       "closing the recovery frame failed");
    } CLEANUP {
	IGNORE(akgl_ui_shutdown());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

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());

	// The headless tests run first, before SDL is up, because running
	// without a renderer is part of what they assert.
	CATCH(errctx, test_ui_validation());
	CATCH(errctx, test_ui_lifecycle());
	CATCH(errctx, test_ui_arena_exhaustion());

	akgl_renderer = &akgl_default_renderer;
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_Init(SDL_INIT_VIDEO),
	    AKGL_ERR_SDL,
	    "Couldn't initialize SDL: %s",
	    SDL_GetError());
	FAIL_ZERO_BREAK(
	    errctx,
	    TTF_Init(),
	    AKGL_ERR_SDL,
	    "Couldn't initialize the font engine: %s",
	    SDL_GetError());
	CATCH(errctx, akgl_registry_init_font());
	CATCH(errctx, akgl_text_loadfont(TEST_FONT_NAME, TEST_FONT_PATH, TEST_FONT_SIZE));
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_CreateWindowAndRenderer(
		"net/aklabs/libakgl/test_ui",
		TEST_TARGET_SIZE,
		TEST_TARGET_SIZE,
		0,
		&akgl_window,
		&akgl_renderer->sdl_renderer),
	    AKGL_ERR_SDL,
	    "Couldn't create window/renderer: %s",
	    SDL_GetError());
	CATCH(errctx, akgl_render_2d_bind(akgl_renderer));

	CATCH(errctx, test_ui_font_register());
	CATCH(errctx, test_ui_measure_text());
	CATCH(errctx, test_ui_frame_bracket());
	CATCH(errctx, test_ui_layout_pixels());
	CATCH(errctx, test_ui_layout_text());
	CATCH(errctx, test_ui_handle_event());
	CATCH(errctx, test_ui_dialog_widget());
	CATCH(errctx, test_ui_label_widget());
	CATCH(errctx, test_ui_menu_widget());
	CATCH(errctx, test_ui_menu_events());
	CATCH(errctx, test_ui_layout_error_surfaces());
    } CLEANUP {
	IGNORE(akgl_text_unloadallfonts());
	TTF_Quit();
	SDL_Quit();
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}
