/**
 * @file controller.c
 * @brief Unit tests for control maps and SDL input dispatch.
 *
 * SDL events are synthesized directly rather than pumped through the event
 * queue, so none of this needs a physical keyboard or gamepad. The device
 * enumeration helpers are exercised against the dummy drivers, where the
 * expected outcome is "no devices, no crash".
 */

#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>

#include <akgl/error.h>
// For the akgl_actor_cmhf_* handlers these tests bind. akgl/controller.h pulls
// actor.h in for itself now; tests/headers.c is what keeps it doing so.
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/game.h>
#include <akgl/controller.h>
#include <akgl/heap.h>
#include <akgl/registry.h>

#include "testutil.h"

/** @brief Keyboard id the tests bind their control maps to. */
#define TEST_KBID 11
/** @brief Gamepad id the tests bind their control maps to. */
#define TEST_JSID 22

/** @brief Stand-in for the application state pointer SDL hands to callbacks. */
static int appstate_placeholder = 0;

/** @brief The actor every control map in this file targets. */
static akgl_Actor *player = NULL;
/** @brief Base character supplying the player's acceleration constants. */
static akgl_Character *playerchar = NULL;

/** @brief Clear every control map so each test starts from an empty binding set. */
static void reset_control_maps(void)
{
    int i = 0;
    for ( i = 0; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
	memset(&akgl_controlmaps[i], 0x00, sizeof(akgl_ControlMap));
    }
}

/** @brief Register the "player" actor the gamepad handlers look up by name. */
static akerr_ErrorContext *make_player(void)
{
    PREPARE_ERROR(e);
    ATTEMPT {
	CATCH(e, akgl_registry_init_actor());
	CATCH(e, akgl_registry_init_character());
	CATCH(e, akgl_heap_init());

	CATCH(e, akgl_heap_next_character(&playerchar));
	CATCH(e, akgl_character_initialize(playerchar, "playerchar"));
	playerchar->ax = 6.0f;
	playerchar->ay = 8.0f;

	CATCH(e, akgl_heap_next_actor(&player));
	CATCH(e, akgl_actor_initialize(player, "player"));
	player->basechar = playerchar;
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/** @brief Build a synthetic keyboard event. */
static void make_key_event(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key)
{
    memset(event, 0x00, sizeof(SDL_Event));
    event->type = type;
    event->key.which = which;
    event->key.key = key;
}

/** @brief Build a synthetic keyboard event carrying modifier state. */
static void make_key_event_mod(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key, SDL_Keymod mod)
{
    make_key_event(event, type, which, key);
    event->key.mod = mod;
}

/**
 * @brief Build the text input event SDL sends after a key press composes.
 *
 * @p text is not copied by SDL or by the library at this point, so callers pass
 * a string literal.
 */
static void make_text_event(SDL_Event *event, const char *text)
{
    memset(event, 0x00, sizeof(SDL_Event));
    event->type = SDL_EVENT_TEXT_INPUT;
    event->text.text = text;
}

/** @brief Build a synthetic gamepad button event. */
static void make_button_event(SDL_Event *event, uint32_t type, SDL_JoystickID which, uint8_t button)
{
    memset(event, 0x00, sizeof(SDL_Event));
    event->type = type;
    event->gbutton.which = which;
    event->gbutton.button = button;
}

akerr_ErrorContext *test_controller_pushmap(void)
{
    PREPARE_ERROR(e);
    akgl_Control control;
    int i = 0;

    ATTEMPT {
	reset_control_maps();
	memset(&control, 0x00, sizeof(akgl_Control));
	control.key = SDLK_SPACE;
	control.event_on = SDL_EVENT_KEY_DOWN;
	control.event_off = SDL_EVENT_KEY_UP;
	control.handler_on = &akgl_actor_cmhf_left_on;
	control.handler_off = &akgl_actor_cmhf_left_off;

	TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing one control");
	TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 1,
		    "pushing one control left nextMap at %d, expected 1",
		    akgl_controlmaps[0].nextMap);
	TEST_ASSERT(e, akgl_controlmaps[0].controls[0].key == SDLK_SPACE,
		    "the pushed control did not land in slot 0");
	TEST_ASSERT(e, akgl_controlmaps[0].controls[0].handler_on == &akgl_actor_cmhf_left_on,
		    "the pushed control lost its press handler");

	// Pushes accumulate rather than overwrite.
	TEST_EXPECT_OK(e, akgl_controller_pushmap(0, &control), "pushing a second control");
	TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 2,
		    "pushing a second control left nextMap at %d, expected 2",
		    akgl_controlmaps[0].nextMap);

	// Maps are independent of each other.
	TEST_EXPECT_OK(e, akgl_controller_pushmap(3, &control), "pushing into a different map");
	TEST_ASSERT(e, akgl_controlmaps[3].nextMap == 1,
		    "map 3 nextMap is %d, expected 1", akgl_controlmaps[3].nextMap);
	TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 2,
		    "pushing into map 3 disturbed map 0 (nextMap %d)", akgl_controlmaps[0].nextMap);

	// Filling a map exactly to capacity still succeeds.
	reset_control_maps();
	for ( i = 0; i < AKGL_MAX_CONTROLS; i++ ) {
	    akerr_ErrorContext *pushresult = akgl_controller_pushmap(0, &control);
	    if ( pushresult != NULL ) {
		pushresult->handled = true;
		pushresult = akerr_release_error(pushresult);
		FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "pushing control %d of %d failed", i, AKGL_MAX_CONTROLS);
	    }
	}
	TEST_ASSERT(e, akgl_controlmaps[0].nextMap == AKGL_MAX_CONTROLS,
		    "a full map reports nextMap %d, expected %d",
		    akgl_controlmaps[0].nextMap, AKGL_MAX_CONTROLS);

	// One past capacity is refused.
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS, akgl_controller_pushmap(0, &control),
			   "pushing into a full control map");

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_pushmap(0, NULL),
			   "pushing a NULL control");
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
			   akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS, &control),
			   "pushing into a control map id at the limit");
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
			   akgl_controller_pushmap(AKGL_MAX_CONTROL_MAPS + 5, &control),
			   "pushing into a control map id past the limit");

	// Negative ids index *before* akgl_controlmaps. Both entry points
	// checked only the upper bound until 0.5.0.
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
			   akgl_controller_pushmap(-1, &control),
			   "pushing into a negative control map id");
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
			   akgl_controller_pushmap(-4096, &control),
			   "pushing into a far negative control map id");
	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
			   akgl_controller_default(-1, "player", TEST_KBID, TEST_JSID),
			   "defaulting a negative control map id");
    } CLEANUP {
	reset_control_maps();
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_default_bindings(void)
{
    PREPARE_ERROR(e);

    ATTEMPT {
	reset_control_maps();
	CATCH(e, make_player());

	TEST_EXPECT_OK(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID),
		       "installing the default control map");

	TEST_ASSERT(e, akgl_controlmaps[0].target == player,
		    "the default map did not target the player actor");
	TEST_ASSERT(e, akgl_controlmaps[0].kbid == TEST_KBID,
		    "the default map recorded keyboard %d, expected %d",
		    akgl_controlmaps[0].kbid, TEST_KBID);
	TEST_ASSERT(e, akgl_controlmaps[0].jsid == TEST_JSID,
		    "the default map recorded gamepad %d, expected %d",
		    akgl_controlmaps[0].jsid, TEST_JSID);

	// Four keyboard bindings then four gamepad bindings.
	TEST_ASSERT(e, akgl_controlmaps[0].nextMap == 8,
		    "the default map installed %d controls, expected 8",
		    akgl_controlmaps[0].nextMap);

	TEST_ASSERT(e, akgl_controlmaps[0].controls[0].key == SDLK_DOWN,
		    "the first default binding is not the down arrow");
	TEST_ASSERT(e, akgl_controlmaps[0].controls[0].handler_on == &akgl_actor_cmhf_down_on,
		    "the down arrow is not bound to the down handler");
	TEST_ASSERT(e, akgl_controlmaps[0].controls[1].key == SDLK_UP,
		    "the second default binding is not the up arrow");
	TEST_ASSERT(e, akgl_controlmaps[0].controls[2].key == SDLK_LEFT,
		    "the third default binding is not the left arrow");
	TEST_ASSERT(e, akgl_controlmaps[0].controls[3].key == SDLK_RIGHT,
		    "the fourth default binding is not the right arrow");

	// The gamepad half binds buttons and leaves the keycode clear, so a
	// keyboard event cannot accidentally match a dpad binding.
	TEST_ASSERT(e, akgl_controlmaps[0].controls[4].button == SDL_GAMEPAD_BUTTON_DPAD_DOWN,
		    "the fifth default binding is not the dpad down button");
	TEST_ASSERT(e, akgl_controlmaps[0].controls[4].key == 0,
		    "a gamepad binding also carries a keycode");
	TEST_ASSERT(e, akgl_controlmaps[0].controls[7].button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
		    "the eighth default binding is not the dpad right button");

	// An unknown actor name is a registry error, not a crash.
	reset_control_maps();
	TEST_EXPECT_STATUS(e, AKGL_ERR_REGISTRY,
			   akgl_controller_default(1, "no_such_actor", TEST_KBID, TEST_JSID),
			   "installing a default map for an unregistered actor");

	TEST_EXPECT_STATUS(e, AKERR_OUTOFBOUNDS,
			   akgl_controller_default(AKGL_MAX_CONTROL_MAPS, "player", TEST_KBID, TEST_JSID),
			   "installing a default map at a control map id past the limit");
    } CLEANUP {
	reset_control_maps();
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_handle_keyboard_events(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;

    ATTEMPT {
	reset_control_maps();
	CATCH(e, make_player());
	CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));

	// Pressing left routes to the left press handler.
	player->state = 0;
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a left key press");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "a left key press did not start the actor moving left (state %d)", player->state);
	TEST_ASSERT_FEQ(e, player->ax, -6.0f,
			"a left key press set ax to %f, expected -6", player->ax);

	// Releasing it routes to the release handler.
	make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a left key release");
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "a left key release did not stop the actor (state %d)", player->state);

	// The other three directions route the same way.
	player->state = 0;
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_RIGHT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a right key press");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
		    "a right key press did not start the actor moving right (state %d)", player->state);

	player->state = 0;
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_UP);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching an up key press");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
		    "an up key press did not start the actor moving up (state %d)", player->state);

	player->state = 0;
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_DOWN);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a down key press");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
		    "a down key press did not start the actor moving down (state %d)", player->state);

	// An unbound key is ignored.
	player->state = 0;
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_F12);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching an unbound key");
	TEST_ASSERT(e, player->state == 0,
		    "an unbound key changed the actor state to %d", player->state);

	// A bound key from a different keyboard is ignored, so split-keyboard
	// local multiplayer does not cross-talk.
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID + 1, SDLK_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a bound key from an unbound keyboard");
	TEST_ASSERT(e, player->state == 0,
		    "a key from another keyboard changed the actor state to %d", player->state);
    } CLEANUP {
	reset_control_maps();
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_handle_gamepad_events(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;

    ATTEMPT {
	reset_control_maps();
	CATCH(e, make_player());
	CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));

	player->state = 0;
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a dpad left press");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "a dpad left press did not start the actor moving left (state %d)", player->state);

	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_UP, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a dpad left release");
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "a dpad left release did not stop the actor (state %d)", player->state);

	player->state = 0;
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a dpad up press");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
		    "a dpad up press did not start the actor moving up (state %d)", player->state);

	// A press from an unbound gamepad is ignored.
	player->state = 0;
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID + 1, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a dpad press from an unbound gamepad");
	TEST_ASSERT(e, player->state == 0,
		    "a press from another gamepad changed the actor state to %d", player->state);

	// An unbound button on the bound gamepad is ignored.
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_START);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching an unbound gamepad button");
	TEST_ASSERT(e, player->state == 0,
		    "an unbound gamepad button changed the actor state to %d", player->state);
    } CLEANUP {
	reset_control_maps();
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/*
 * A control map bound to keyboard 0 hears every keyboard.
 *
 * There is no way to enumerate "the keyboard the player will actually type on".
 * SDL_GetKeyboards() reports what is attached, but the id a key event carries is
 * decided by the video backend: X11 without XInput2 sends
 * SDL_GLOBAL_KEYBOARD_ID, which is 0, while X11 *with* XInput2 sends the
 * physical slave device's sourceid -- and neither is the SDL_DEFAULT_KEYBOARD_ID
 * of 1 that SDL_AddKeyboard registers when XInput2 is missing. A game that binds
 * SDL_GetKeyboards()[0] therefore gets a map that matches nothing at all.
 *
 * 0 is the right spelling for "any", because SDL documents `which` as 0 when the
 * source is unknown or virtual, so no real device can be 0 and nothing is given
 * up by spending it. The same argument applies to a joystick id of 0.
 *
 * Every other test in this suite dispatches an event whose id equals the id the
 * map was bound with, so none of them can see this.
 */
akerr_ErrorContext *test_controller_wildcard_device_ids(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;

    ATTEMPT {
	reset_control_maps();
	CATCH(e, make_player());
	// 0 for both: "whatever keyboard and whatever gamepad".
	CATCH(e, akgl_controller_default(0, "player", 0, 0));

	// A key event from a real XInput2 device id must still be heard.
	player->state = 0;
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a left key press from a real device id");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "a map bound to keyboard 0 ignored a key press from device %d (state %d)",
		    TEST_KBID, player->state);

	// And so must one carrying SDL_GLOBAL_KEYBOARD_ID, which is 0.
	player->state = 0;
	make_key_event(&event, SDL_EVENT_KEY_DOWN, 0, SDLK_RIGHT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a right key press from the global keyboard");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
		    "a map bound to keyboard 0 ignored a key press from keyboard 0 (state %d)",
		    player->state);

	/*
	 * A map bound to a *specific* keyboard still hears only that one. This is
	 * the half that keeps two local players on two keyboards apart, and the
	 * wildcard must not cost it.
	 */
	reset_control_maps();
	CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
	player->state = 0;
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID + 1, SDLK_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a left key press from the wrong device");
	TEST_ASSERT(e, (player->state == 0),
		    "a map bound to keyboard %d acted on a press from %d (state %d)",
		    TEST_KBID, (TEST_KBID + 1), player->state);
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);

    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_handle_event_edge_cases(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;

    ATTEMPT {
	reset_control_maps();
	CATCH(e, make_player());

	// With no control maps installed at all, every event is a no-op.
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching into an empty control map table");

	// A map in a later slot is still reached, so the scan does not stop at
	// the first unpopulated entry.
	CATCH(e, akgl_controller_default(5, "player", TEST_KBID, TEST_JSID));
	player->state = 0;
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching to a control map in a later slot");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "a control map in slot 5 was not consulted (state %d)", player->state);

	// An event type that no binding uses falls through harmlessly.
	memset(&event, 0x00, sizeof(SDL_Event));
	event.type = SDL_EVENT_MOUSE_MOTION;
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching an event type with no bindings");

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_event(NULL, &event),
			   "dispatching with a NULL appstate");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_controller_handle_event(&appstate_placeholder, NULL),
			   "dispatching a NULL event");
    } CLEANUP {
	reset_control_maps();
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_gamepad_button_handlers(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;

    ATTEMPT {
	CATCH(e, make_player());

	// These handlers find their actor by looking up "player" in the registry
	// rather than taking it as an argument.
	player->state = 0;
	player->movement_controls_face = false;
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
	TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event),
		       "dpad down press handler");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
		    "the dpad down handler did not set MOVING_DOWN (state %d)", player->state);
	// With automatic facing off, the handler sets the facing itself.
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
		    "the dpad down handler did not set FACE_DOWN (state %d)", player->state);

	TEST_EXPECT_OK(e, akgl_controller_handle_button_up(&appstate_placeholder, &event),
		       "dpad down release handler");
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_MOVING_DOWN),
		    "the dpad down release handler did not clear MOVING_DOWN (state %d)", player->state);
	TEST_ASSERT(e, player->curSpriteFrameId == 0,
		    "the release handler did not reset the animation frame");

	player->state = 0;
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_UP);
	TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event), "dpad up press handler");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_UP),
		    "the dpad up handler did not set MOVING_UP (state %d)", player->state);
	TEST_EXPECT_OK(e, akgl_controller_handle_button_up(&appstate_placeholder, &event), "dpad up release handler");

	player->state = 0;
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event), "dpad left press handler");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "the dpad left handler did not set MOVING_LEFT (state %d)", player->state);
	TEST_EXPECT_OK(e, akgl_controller_handle_button_up(&appstate_placeholder, &event), "dpad left release handler");

	player->state = 0;
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
	TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event), "dpad right press handler");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT),
		    "the dpad right handler did not set MOVING_RIGHT (state %d)", player->state);
	TEST_EXPECT_OK(e, akgl_controller_handle_button_up(&appstate_placeholder, &event), "dpad right release handler");

	// With automatic facing on, the handler leaves facing to the actor's own
	// face function.
	player->state = 0;
	player->movement_controls_face = true;
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
	TEST_EXPECT_OK(e, akgl_controller_handle_button_down(&appstate_placeholder, &event),
		       "dpad down press with automatic facing on");
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(player->state, AKGL_ACTOR_STATE_FACE_DOWN),
		    "the handler set facing even though the actor faces automatically (state %d)",
		    player->state);

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_button_down(NULL, &event),
			   "dpad press handler with a NULL appstate");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_controller_handle_button_down(&appstate_placeholder, NULL),
			   "dpad press handler with a NULL event");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_button_up(NULL, &event),
			   "dpad release handler with a NULL appstate");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_controller_handle_button_up(&appstate_placeholder, NULL),
			   "dpad release handler with a NULL event");

	// With no actor named "player" registered there is nothing to drive.
	CATCH(e, akgl_registry_init_actor());
	make_button_event(&event, SDL_EVENT_GAMEPAD_BUTTON_DOWN, TEST_JSID, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_controller_handle_button_down(&appstate_placeholder, &event),
			   "dpad press handler with no player actor registered");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_controller_handle_button_up(&appstate_placeholder, &event),
			   "dpad release handler with no player actor registered");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_device_events(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;

    ATTEMPT {
	// The dummy drivers present no gamepads, so the add and remove handlers
	// take their "unknown device" paths. Neither may crash.
	make_button_event(&event, SDL_EVENT_GAMEPAD_ADDED, 999, 0);
	TEST_EXPECT_OK(e, akgl_controller_handle_added(&appstate_placeholder, &event),
		       "handling an add for a device that cannot be opened");

	make_button_event(&event, SDL_EVENT_GAMEPAD_REMOVED, 999, 0);
	TEST_EXPECT_OK(e, akgl_controller_handle_removed(&appstate_placeholder, &event),
		       "handling a remove for a device that was never open");

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_added(NULL, &event),
			   "add handler with a NULL appstate");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_controller_handle_added(&appstate_placeholder, NULL),
			   "add handler with a NULL event");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_handle_removed(NULL, &event),
			   "remove handler with a NULL appstate");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_controller_handle_removed(&appstate_placeholder, NULL),
			   "remove handler with a NULL event");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_device_enumeration(void)
{
    PREPARE_ERROR(e);

    ATTEMPT {
	// Enumeration has to succeed on a machine with no input devices, which
	// is the normal state under the dummy drivers and in CI.
	TEST_EXPECT_OK(e, akgl_controller_list_keyboards(), "listing keyboards");
	TEST_EXPECT_OK(e, akgl_controller_open_gamepads(), "opening gamepads");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_poll_key(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;
    int keycode = -1;
    bool available = true;

    ATTEMPT {
	reset_control_maps();
	CATCH(e, make_player());
	CATCH(e, akgl_controller_flush_keys());

	// An empty buffer answers "no key waiting" and succeeds.
	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
		       "polling an empty key buffer");
	TEST_ASSERT(e, available == false,
		    "polling an empty buffer reported a key was available");
	TEST_ASSERT(e, keycode == 0,
		    "polling an empty buffer left keycode at %d, expected 0", keycode);

	// A key press pumped through the handler is drained by the poller,
	// exactly once.
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a key press with no control maps installed");
	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling for that key");
	TEST_ASSERT(e, available == true, "the pressed key was not available to the poller");
	TEST_ASSERT(e, keycode == SDLK_A,
		    "the poller returned keycode %d, expected %d", keycode, (int)SDLK_A);
	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling again");
	TEST_ASSERT(e, available == false, "the same keystroke was delivered twice");

	// Key releases are not keystrokes.
	make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_A);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a key release");
	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a release");
	TEST_ASSERT(e, available == false, "a key release was buffered as a keystroke");

	// Keys come back in the order they were pressed.
	CATCH(e, akgl_controller_flush_keys());
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_1);
	CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_2);
	CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_3);
	CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));

	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the first key");
	TEST_ASSERT(e, keycode == SDLK_1, "the first key out was %d, expected %d",
		    keycode, (int)SDLK_1);
	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the second key");
	TEST_ASSERT(e, keycode == SDLK_2, "the second key out was %d, expected %d",
		    keycode, (int)SDLK_2);
	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the third key");
	TEST_ASSERT(e, keycode == SDLK_3, "the third key out was %d, expected %d",
		    keycode, (int)SDLK_3);

	// A key that a control map also acts on still reaches the poller, so a
	// game and an embedded interpreter can share one keyboard.
	CATCH(e, akgl_controller_flush_keys());
	CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
	player->state = 0;
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a bound key press");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "the bound key stopped driving the actor (state %d)", player->state);
	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
		       "polling for a key a control map claimed");
	TEST_ASSERT(e, available == true, "a bound key never reached the key buffer");
	TEST_ASSERT(e, keycode == SDLK_LEFT, "the poller returned keycode %d, expected %d",
		    keycode, (int)SDLK_LEFT);

	// Flushing discards the backlog.
	CATCH(e, akgl_controller_flush_keys());
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B);
	CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
	TEST_EXPECT_OK(e, akgl_controller_flush_keys(), "flushing a buffer with a key in it");
	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a flush");
	TEST_ASSERT(e, available == false, "a flush left a keystroke behind");

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(NULL, &available),
			   "polling into a NULL keycode");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(&keycode, NULL),
			   "polling into a NULL availability flag");
    } CLEANUP {
	reset_control_maps();
	IGNORE(akgl_controller_flush_keys());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_poll_key_overflow(void)
{
    PREPARE_ERROR(e);
    SDL_Event event;
    int keycode = -1;
    bool available = true;
    bool ordered = true;
    bool pumped = true;
    int i = 0;

    ATTEMPT {
	reset_control_maps();
	CATCH(e, akgl_controller_flush_keys());

	// Fill the buffer exactly, then press one more. The overflowing key is
	// the one that is dropped -- what was typed first survives.
	for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER + 1; i++ ) {
	    akerr_ErrorContext *pumpresult = NULL;
	    make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A + i);
	    pumpresult = akgl_controller_handle_event(&appstate_placeholder, &event);
	    if ( pumpresult != NULL ) {
		pumpresult->handled = true;
		pumpresult = akerr_release_error(pumpresult);
		pumped = false;
	    }
	}
	TEST_ASSERT(e, pumped == true, "dispatching the overflow key presses failed");

	for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER; i++ ) {
	    akerr_ErrorContext *pollresult = akgl_controller_poll_key(&keycode, &available);
	    if ( pollresult != NULL ) {
		pollresult->handled = true;
		pollresult = akerr_release_error(pollresult);
		ordered = false;
	    }
	    TEST_ASSERT_FLAG(ordered, available == true);
	    TEST_ASSERT_FLAG(ordered, keycode == (int)(SDLK_A + i));
	}
	TEST_ASSERT(e, ordered == true,
		    "a full buffer did not return the first %d keys in order",
		    AKGL_CONTROLLER_KEY_BUFFER);

	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
		       "polling after draining a full buffer");
	TEST_ASSERT(e, available == false,
		    "the key pressed past capacity was buffered anyway (keycode %d)", keycode);

	// The buffer is reusable after an overflow rather than wedged.
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_Z);
	TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
		       "dispatching a key after an overflow");
	TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
		       "polling after an overflow");
	TEST_ASSERT(e, available == true, "the buffer stayed full after being drained");
	TEST_ASSERT(e, keycode == SDLK_Z, "the poller returned keycode %d, expected %d",
		    keycode, (int)SDLK_Z);
    } CLEANUP {
	reset_control_maps();
	IGNORE(akgl_controller_flush_keys());
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_controller_poll_keystroke(void)
{
    PREPARE_ERROR(errctx);
    SDL_Event event;
    akgl_Keystroke keystroke;
    int keycode = -1;
    bool available = true;

    ATTEMPT {
	reset_control_maps();
	CATCH(errctx, akgl_controller_flush_keys());

	// An empty buffer answers "nothing waiting" and zeroes the destination.
	memset(&keystroke, 0xff, sizeof(akgl_Keystroke));
	TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
		       "polling an empty keystroke buffer");
	TEST_ASSERT(errctx, available == false,
		    "polling an empty buffer reported a keystroke was available");
	TEST_ASSERT(errctx, keystroke.key == 0,
		    "polling an empty buffer left a keycode of %d behind", (int)keystroke.key);
	TEST_ASSERT(errctx, keystroke.text[0] == '\0',
		    "polling an empty buffer left text behind");

	// The case the whole thing exists for: a shifted key. The keycode is
	// the unshifted one, so the composed character is the only place the
	// double quote can come from.
	make_key_event_mod(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_APOSTROPHE, SDL_KMOD_LSHIFT);
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	make_text_event(&event, "\"");
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));

	TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
		       "polling for a shifted key");
	TEST_ASSERT(errctx, available == true, "a shifted key press never reached the buffer");
	TEST_ASSERT(errctx, keystroke.key == SDLK_APOSTROPHE,
		    "the keystroke reported keycode %d, expected %d",
		    (int)keystroke.key, (int)SDLK_APOSTROPHE);
	TEST_ASSERT(errctx, (keystroke.mod & SDL_KMOD_SHIFT) != 0,
		    "the keystroke lost its shift state (mod %d)", (int)keystroke.mod);
	TEST_ASSERT(errctx, strcmp(keystroke.text, "\"") == 0,
		    "the keystroke composed to \"%s\", expected a double quote", keystroke.text);

	// A key that composes to nothing still arrives, with empty text -- that
	// is how a line editor recognises Backspace and the arrows.
	CATCH(errctx, akgl_controller_flush_keys());
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
		       "polling for a non-printing key");
	TEST_ASSERT(errctx, keystroke.key == SDLK_LEFT,
		    "a non-printing key reported keycode %d, expected %d",
		    (int)keystroke.key, (int)SDLK_LEFT);
	TEST_ASSERT(errctx, keystroke.text[0] == '\0',
		    "a non-printing key composed to \"%s\", expected nothing", keystroke.text);

	// Text with no key press behind it -- an input method, or a character
	// finished by a dead key -- is buffered on its own rather than dropped.
	CATCH(errctx, akgl_controller_flush_keys());
	make_text_event(&event, "e");
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
		       "polling for composed text with no key press");
	TEST_ASSERT(errctx, available == true, "composed text with no key press was dropped");
	TEST_ASSERT(errctx, keystroke.key == 0,
		    "text with no key press reported keycode %d, expected 0", (int)keystroke.key);
	TEST_ASSERT(errctx, strcmp(keystroke.text, "e") == 0,
		    "text with no key press composed to \"%s\", expected \"e\"", keystroke.text);
	TEST_ASSERT(errctx, keystroke.mod == 0,
		    "a text-only entry reported modifiers %d; the character it carries already "
		    "reflects them", (int)keystroke.mod);

	// The keycode form has nowhere to report that entry, so it discards it
	// on the way past rather than handing back a keystroke with no key.
	CATCH(errctx, akgl_controller_flush_keys());
	make_text_event(&event, "e");
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B);
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available),
		       "polling by keycode past a text-only entry");
	TEST_ASSERT(errctx, available == true, "the key behind a text-only entry was lost");
	TEST_ASSERT(errctx, keycode == SDLK_B,
		    "polling by keycode returned %d, expected %d", keycode, (int)SDLK_B);

	// Both pollers drain the same buffer.
	CATCH(errctx, akgl_controller_flush_keys());
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_C);
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available));
	TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available),
		       "polling by keycode after the same key was taken as a keystroke");
	TEST_ASSERT(errctx, available == false, "one keystroke was delivered to both pollers");

	// Text that arrives after its press has already been drained becomes an
	// entry of its own instead of being attached to somebody else's key.
	CATCH(errctx, akgl_controller_flush_keys());
	make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_D);
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available));
	make_text_event(&event, "d");
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
		       "polling for text that outlived its key press");
	TEST_ASSERT(errctx, available == true, "text that outlived its key press was dropped");
	TEST_ASSERT(errctx, keystroke.key == 0,
		    "text that outlived its key press was attached to keycode %d",
		    (int)keystroke.key);

	// More text than an entry holds is cut on a character boundary, never
	// through the middle of one. Each of these is two bytes, so three fit
	// in the eight-byte field and the fourth does not.
	CATCH(errctx, akgl_controller_flush_keys());
	make_text_event(&event, "\xce\xb1\xce\xb1\xce\xb1\xce\xb1");
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
		       "polling for text longer than one entry holds");
	TEST_ASSERT(errctx, strcmp(keystroke.text, "\xce\xb1\xce\xb1\xce\xb1") == 0,
		    "oversized text was truncated to \"%s\", expected three whole characters",
		    keystroke.text);

	// A sequence the string ends in the middle of is dropped rather than
	// copied by reading past the terminator to find its other half.
	CATCH(errctx, akgl_controller_flush_keys());
	make_text_event(&event, "a\xce");
	CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
	TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
		       "polling for text that ends mid-character");
	TEST_ASSERT(errctx, strcmp(keystroke.text, "a") == 0,
		    "a truncated character came back as \"%s\", expected just the whole one",
		    keystroke.text);

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_controller_poll_keystroke(NULL, &available),
			   "polling into a NULL keystroke");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_controller_poll_keystroke(&keystroke, NULL),
			   "polling into a NULL availability flag");
    } CLEANUP {
	reset_control_maps();
	IGNORE(akgl_controller_flush_keys());
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    PREPARE_ERROR(errctx);

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

    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD),
	    AKGL_ERR_SDL,
	    "Couldn't initialize SDL: %s",
	    SDL_GetError());

	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_registry_init());

	CATCH(errctx, test_controller_pushmap());
	CATCH(errctx, test_controller_default_bindings());
	CATCH(errctx, test_controller_handle_keyboard_events());
	CATCH(errctx, test_controller_handle_gamepad_events());
	CATCH(errctx, test_controller_wildcard_device_ids());
	CATCH(errctx, test_controller_handle_event_edge_cases());
	CATCH(errctx, test_controller_gamepad_button_handlers());
	CATCH(errctx, test_controller_device_events());
	CATCH(errctx, test_controller_device_enumeration());
	CATCH(errctx, test_controller_poll_key());
	CATCH(errctx, test_controller_poll_key_overflow());
	CATCH(errctx, test_controller_poll_keystroke());
    } CLEANUP {
	SDL_Quit();
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}
