#define UNHANDLED_ERROR_TERMINATION_BEHAVIOR	\
    handle_unhandled_error(errctx);

#include <akerror.h>
#include <akgl/error.h>

#define UNHANDLED_ERROR_EXIT 0
#define UNHANDLED_ERROR_SET  1

#include <SDL3/SDL.h>
#include <stdlib.h>

#include <akgl/iterator.h>
#include <akgl/registry.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/sprite.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/renderer.h>

#include "testutil.h"

int UNHANDLED_ERROR_BEHAVIOR;
akerr_ErrorContext *unhandled_error_context;

void handle_unhandled_error_noexit(akerr_ErrorContext *errctx)
{
    if ( errctx == NULL ) {
	return;
    }
    if ( UNHANDLED_ERROR_BEHAVIOR == UNHANDLED_ERROR_EXIT ) {
	// akerr_exit rather than exit(3): a status past 255 does not survive a
	// wait status, and this suite's are AKGL_* codes, which all are.
	akerr_exit(errctx->status);
    }
    if ( UNHANDLED_ERROR_BEHAVIOR == UNHANDLED_ERROR_SET ) {
	unhandled_error_context = errctx;
	errctx->refcount += 1;
	return;
    }
}

int akgl_actor_updated;
akerr_ErrorContext *akgl_actor_update_noop(akgl_Actor *obj)
{
    PREPARE_ERROR(errctx);
    akgl_actor_updated = 1;
    SUCCEED_RETURN(errctx);
}

// Currently the renderer assumes there is a global variable named `renderer`
int akgl_actor_rendered;
akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
{
    PREPARE_ERROR(errctx);
    akgl_actor_rendered = 1;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_registry_actor_iterator_nullpointers(void)
{
    PREPARE_ERROR(errctx);

    akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;

    akerr_handler_unhandled_error = handle_unhandled_error_noexit;
    ATTEMPT {
	UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
	DETECT(unhandled_error_context, akgl_registry_iterate_actor(NULL, AKGL_REGISTRY_ACTOR, ""));
    } CLEANUP {
	UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
    } PROCESS(unhandled_error_context) {
    } HANDLE(unhandled_error_context, AKERR_NULLPOINTER) {
	printf("Handled\n");
    } FINISH(unhandled_error_context, true);
    akerr_handler_unhandled_error = defaulthandler;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_registry_actor_iterator_missingactor(void)
{
    PREPARE_ERROR(errctx);
    akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;

    akgl_Iterator iter = {
	.layerid = 0,
	.flags = 0
    };
    akerr_handler_unhandled_error = handle_unhandled_error_noexit;
    ATTEMPT {
	UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
	DETECT(
	    unhandled_error_context,
	    akgl_registry_iterate_actor(
		&iter,
		AKGL_REGISTRY_ACTOR,
		"Actor who doesn't exist")
	    );
    } CLEANUP {
	UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
    } PROCESS(unhandled_error_context) {
    } HANDLE(unhandled_error_context, AKERR_KEY) {
	printf("Handled\n");
    } FINISH(unhandled_error_context, true);
    akerr_handler_unhandled_error = defaulthandler;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
{
    akgl_Actor *testactor;
    akgl_Iterator iter = {
	.layerid = 0,
	.flags = AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_RENDER
    };
    akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;

    PREPARE_ERROR(errctx);
    akerr_handler_unhandled_error = handle_unhandled_error_noexit;
    ATTEMPT {
	UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_SET;
	CATCH(unhandled_error_context, akgl_heap_next_actor(&testactor));
	CATCH(unhandled_error_context, akgl_actor_initialize(testactor, "test"));

	testactor->layer = 0;
	testactor->updatefunc = &akgl_actor_update_noop;
	testactor->renderfunc = &akgl_actor_render_noop;

	DETECT(
	    unhandled_error_context,
	    akgl_registry_iterate_actor(
		&iter,
		AKGL_REGISTRY_ACTOR,
		"test")
	    );

	FAIL_ZERO_BREAK(
	    unhandled_error_context,
	    akgl_actor_updated,
	    AKGL_ERR_BEHAVIOR,
	    "actor->updatefunc not called by the iterator"
	    );
	FAIL_ZERO_BREAK(
	    unhandled_error_context,
	    akgl_actor_rendered,
	    AKGL_ERR_BEHAVIOR,
	    "actor->renderfunc not called by the iterator"
	    );
    } CLEANUP {
	UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
	IGNORE(akgl_heap_release_actor(testactor));
    } PROCESS(unhandled_error_context) {
    } FINISH(unhandled_error_context, true);

    akerr_handler_unhandled_error = defaulthandler;

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_actor_set_character(void)
{
    akgl_Actor *testactor = NULL;
    akgl_Character *testchar = NULL;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_actor_set_character(NULL, "test"));
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_NULLPOINTER) {
	printf("Handled\n");
    } FINISH(errctx, true);


    ATTEMPT {
	CATCH(errctx, akgl_heap_next_actor(&testactor));

	CATCH(errctx, akgl_actor_initialize(testactor, "test"));
	testactor->layer = 0;
	testactor->updatefunc = &akgl_actor_update_noop;
	testactor->renderfunc = &akgl_actor_render_noop;

	CATCH(errctx, akgl_actor_set_character(testactor, "test"));
    } CLEANUP {
	IGNORE(akgl_heap_release_actor(testactor));
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_NULLPOINTER) {
	printf("Handled\n");
    } FINISH(errctx, true);

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_actor(&testactor));
	CATCH(errctx, akgl_heap_next_character(&testchar));

	CATCH(errctx, akgl_actor_initialize(testactor, "test"));
	testactor->layer = 0;
	testactor->updatefunc = &akgl_actor_update_noop;
	testactor->renderfunc = &akgl_actor_render_noop;

	CATCH(errctx, akgl_character_initialize(testchar, "test"));

	CATCH(errctx, akgl_actor_set_character(testactor, "test"));
    } CLEANUP {
	IGNORE(akgl_heap_release_actor(testactor));
	IGNORE(akgl_heap_release_character(testchar));
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_actor_manage_children(void)
{
    akgl_Actor *parent = NULL;
    akgl_Actor *child = NULL;
    akgl_String *tmpstring = NULL;
    int i = 0;
    int j = 0;
    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_heap_next_string(&tmpstring));
	CATCH(errctx, akgl_heap_next_actor(&parent));
	CATCH(errctx, akgl_actor_initialize(parent, "parent"));
	for ( i = 0 ; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
	    sprintf((char *)&tmpstring->data, "child %d", i);
	    CATCH(errctx, akgl_heap_next_actor(&child));
	    CATCH(errctx, akgl_actor_initialize(child, (char *)&tmpstring->data));
	    CATCH(errctx, parent->addchild(parent, child));
	    // Release our claim on the actor so the parent can own the child and kill it
	    CATCH(errctx, akgl_heap_release_actor(child));
	}
    } CLEANUP {
	IGNORE(akgl_heap_release_string(tmpstring));
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    ATTEMPT {
	// Doesn't matter which child we use for this test
	child = parent->children[1];
	CATCH(errctx, parent->addchild(parent, child));
    } CLEANUP {
	if ( errctx == NULL ) {
	    FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_RELATIONSHIP when child already has a parent");
	}
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_RELATIONSHIP) {
	// Expected behavior
	SDL_Log("addchild throws AKERR_RELATIONSHIP when child already has a parent");
    } FINISH(errctx, true);

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_actor(&child));
	CATCH(errctx, parent->addchild(parent, child));
    } CLEANUP {
	if ( errctx == NULL ) {
	    FAIL(errctx, AKGL_ERR_BEHAVIOR, "addchild does not throw AKERR_OUTOFBOUNDS when all children already set");
	}
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_OUTOFBOUNDS) {
	// Expected behavior
	SDL_Log("addchild throws AKERR_OUTOFBOUNDS when all children already set");
    } FINISH(errctx, true);

    ATTEMPT {
	CATCH(errctx, akgl_heap_release_actor(parent));
	// All actor objects on the heap should be empty now
	for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++) {
	    FAIL_NONZERO_BREAK(errctx, akgl_heap_actors[i].refcount, AKERR_VALUE, "Actor not properly cleared");
	    FAIL_NONZERO_BREAK(errctx, akgl_heap_actors[i].parent, AKERR_VALUE, "Actor not properly cleared");
	    for ( j = 0 ; j < AKGL_ACTOR_MAX_CHILDREN; j++) {
		if ( akgl_heap_actors[i].children[j] != NULL ) {
		    FAIL(errctx, AKERR_VALUE, "Actor not properly cleared");
		    goto _test_actor_addchild_heaprelease_cleanup;
		}
	    }
	}
	for ( j = 0; j < AKGL_ACTOR_MAX_CHILDREN; j++) {
	    sprintf((char *)&tmpstring->data, "child %d", i);
	    FAIL_NONZERO_BREAK(
		errctx,
		SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, (char *)&tmpstring->data, NULL),
		AKERR_KEY,
		"Child %s was not removed from the registry",
		(char *)&tmpstring->data);
	}
    _test_actor_addchild_heaprelease_cleanup:
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_actor(&parent));
	CATCH(errctx, akgl_actor_initialize(parent, "parent"));
	CATCH(errctx, akgl_heap_next_actor(&child));
	CATCH(errctx, akgl_actor_initialize(child, "child"));
	// Don't release our claim on the child. The child should not be reclaimed.
	CATCH(errctx, akgl_heap_release_actor(parent));
	FAIL_NONZERO_BREAK(errctx, child->parent, AKERR_VALUE, "Child still references released parent");
	FAIL_ZERO_BREAK(errctx, child->refcount, AKERR_VALUE, "Child prematurely released");
	FAIL_NONZERO_BREAK(errctx, strcmp(child->name, "child"), AKERR_VALUE, "Child had identity erased");
	FAIL_ZERO_BREAK(
	    errctx,
	    (child == SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, child->name, NULL)),
	    AKERR_KEY,
	    "Child prematurely removed from the registry");
	// Now we can release the child
	CATCH(errctx, akgl_heap_release_actor(child));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

/**
 * @brief Build an actor bound to a character, without touching the renderer.
 *
 * The sprite and spritesheet are taken straight off the heap and populated by
 * hand, because akgl_sprite_load_json() would need a live renderer to build a
 * texture and none of the logic under test reads one.
 */
static akerr_ErrorContext *make_bound_actor(
    akgl_Actor **actor,
    akgl_Character **basechar,
    akgl_Sprite **sprite,
    char *name,
    int state)
{
    PREPARE_ERROR(e);
    akgl_SpriteSheet *sheet = NULL;
    char spritename[AKGL_SPRITE_MAX_NAME_LENGTH];

    ATTEMPT {
	CATCH(e, akgl_heap_next_character(basechar));
	CATCH(e, akgl_character_initialize(*basechar, name));

	CATCH(e, akgl_heap_next_spritesheet(&sheet));
	sheet->refcount += 1;

	snprintf((char *)&spritename, AKGL_SPRITE_MAX_NAME_LENGTH, "%s_sprite", name);
	CATCH(e, akgl_heap_next_sprite(sprite));
	CATCH(e, akgl_sprite_initialize(*sprite, (char *)&spritename, sheet));
	(*sprite)->width = 32;
	(*sprite)->height = 32;
	(*sprite)->frames = 4;
	(*sprite)->speed = 100;
	CATCH(e, akgl_character_sprite_add(*basechar, *sprite, state));

	CATCH(e, akgl_heap_next_actor(actor));
	CATCH(e, akgl_actor_initialize(*actor, name));
	(*actor)->basechar = *basechar;
	(*actor)->state = state;
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_actor_control_handlers_on(void)
{
    PREPARE_ERROR(e);
    akgl_Actor actor;
    akgl_Character basechar;
    SDL_Event event;

    ATTEMPT {
	memset(&event, 0x00, sizeof(SDL_Event));
	memset(&basechar, 0x00, sizeof(akgl_Character));
	basechar.ax = 7.0f;
	basechar.ay = 11.0f;

	// Left: clears every facing and movement bit, then sets its own pair and
	// takes acceleration from the base character with the sign reversed.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.basechar = &basechar;
	actor.state = (AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_ALIVE);
	TEST_EXPECT_OK(e, akgl_actor_cmhf_left_on(&actor, &event), "left on");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "left on did not set MOVING_LEFT (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
		    "left on did not set FACE_LEFT (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
		    "left on left MOVING_RIGHT set (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
		    "left on left FACE_RIGHT set (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
		    "left on cleared an unrelated state bit (state %d)", actor.state);
	TEST_ASSERT_FEQ(e, actor.ax, -7.0f, "left on set ax to %f, expected -7", actor.ax);

	// Right: same, with the sign preserved.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.basechar = &basechar;
	TEST_EXPECT_OK(e, akgl_actor_cmhf_right_on(&actor, &event), "right on");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
		    "right on did not set MOVING_RIGHT (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
		    "right on did not set FACE_RIGHT (state %d)", actor.state);
	TEST_ASSERT_FEQ(e, actor.ax, 7.0f, "right on set ax to %f, expected 7", actor.ax);

	// Up reverses the Y acceleration, because Y grows downward.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.basechar = &basechar;
	TEST_EXPECT_OK(e, akgl_actor_cmhf_up_on(&actor, &event), "up on");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
		    "up on did not set MOVING_UP (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
		    "up on did not set FACE_UP (state %d)", actor.state);
	TEST_ASSERT_FEQ(e, actor.ay, -11.0f, "up on set ay to %f, expected -11", actor.ay);

	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.basechar = &basechar;
	TEST_EXPECT_OK(e, akgl_actor_cmhf_down_on(&actor, &event), "down on");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
		    "down on did not set MOVING_DOWN (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
		    "down on did not set FACE_DOWN (state %d)", actor.state);
	TEST_ASSERT_FEQ(e, actor.ay, 11.0f, "down on set ay to %f, expected 11", actor.ay);

	// Each direction is exclusive: turning right after left leaves only right.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.basechar = &basechar;
	TEST_EXPECT_OK(e, akgl_actor_cmhf_left_on(&actor, &event), "left on before turning");
	TEST_EXPECT_OK(e, akgl_actor_cmhf_right_on(&actor, &event), "right on after left");
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "turning right left MOVING_LEFT set (state %d)", actor.state);
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_actor_control_handlers_off(void)
{
    PREPARE_ERROR(e);
    akgl_Actor actor;
    akgl_Character basechar;
    SDL_Event event;

    ATTEMPT {
	memset(&event, 0x00, sizeof(SDL_Event));
	memset(&basechar, 0x00, sizeof(akgl_Character));
	basechar.ax = 7.0f;
	basechar.ay = 11.0f;

	// Releasing a direction stops the actor pushing: acceleration and thrust
	// go to zero. It does *not* touch the environmental force on that axis.
	//
	// It used to zero `e` and `v` as well, and that is a real bug rather
	// than a style difference: `ey` is where the arcade backend accumulates
	// gravity, so tapping down while falling zeroed the fall and the
	// character stopped dead in mid-air. `tests/physics_sim.c` measures it
	// (`vertical release keeps gravity`). Velocity is not the actor's to
	// clear either -- akgl_physics_simulate recomputes `vx` as `ex + tx`
	// every step, so whatever a handler writes there is overwritten before
	// anything can read it.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.basechar = &basechar;
	actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
	actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
	actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_LEFT);
	TEST_EXPECT_OK(e, akgl_actor_cmhf_left_off(&actor, &event), "left off");
	TEST_ASSERT_FEQ(e, actor.ax, 0.0f, "left off left ax at %f", actor.ax);
	TEST_ASSERT_FEQ(e, actor.tx, 0.0f, "left off left tx at %f", actor.tx);
	TEST_ASSERT_FEQ(e, actor.ex, 5.0f, "left off cleared ex (%f), which is the world's, not the actor's", actor.ex);
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_LEFT),
		    "left off did not clear MOVING_LEFT (state %d)", actor.state);
	// Facing is deliberately sticky, so an idle actor keeps looking where it was.
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
		    "left off cleared FACE_LEFT, which should persist (state %d)", actor.state);
	// The Y axis is untouched by a horizontal release.
	TEST_ASSERT_FEQ(e, actor.ay, 5.0f, "left off disturbed ay (%f)", actor.ay);
	TEST_ASSERT_FEQ(e, actor.ey, 5.0f, "left off disturbed ey (%f)", actor.ey);
	TEST_ASSERT_FEQ(e, actor.ty, 5.0f, "left off disturbed ty (%f)", actor.ty);

	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.ax = 5; actor.ex = 5; actor.tx = 5; actor.vx = 5;
	actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
	TEST_EXPECT_OK(e, akgl_actor_cmhf_right_off(&actor, &event), "right off");
	TEST_ASSERT_FEQ(e, actor.ax, 0.0f, "right off left ax at %f", actor.ax);
	TEST_ASSERT_FEQ(e, actor.tx, 0.0f, "right off left tx at %f", actor.tx);
	TEST_ASSERT_FEQ(e, actor.ex, 5.0f, "right off cleared ex (%f)", actor.ex);
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_RIGHT),
		    "right off did not clear MOVING_RIGHT (state %d)", actor.state);

	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
	actor.state = AKGL_ACTOR_STATE_MOVING_UP;
	TEST_EXPECT_OK(e, akgl_actor_cmhf_up_off(&actor, &event), "up off");
	TEST_ASSERT_FEQ(e, actor.ay, 0.0f, "up off left ay at %f", actor.ay);
	TEST_ASSERT_FEQ(e, actor.ty, 0.0f, "up off left ty at %f", actor.ty);
	TEST_ASSERT_FEQ(e, actor.ey, 5.0f, "up off cleared ey (%f), which is where gravity accumulates", actor.ey);
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_UP),
		    "up off did not clear MOVING_UP (state %d)", actor.state);

	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.ay = 5; actor.ey = 5; actor.ty = 5; actor.vy = 5;
	actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
	TEST_EXPECT_OK(e, akgl_actor_cmhf_down_off(&actor, &event), "down off");
	TEST_ASSERT_FEQ(e, actor.ay, 0.0f, "down off left ay at %f", actor.ay);
	TEST_ASSERT_FEQ(e, actor.ty, 0.0f, "down off left ty at %f", actor.ty);
	TEST_ASSERT_FEQ(e, actor.ey, 5.0f, "down off cleared ey (%f), which is where gravity accumulates", actor.ey);
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_MOVING_DOWN),
		    "down off did not clear MOVING_DOWN (state %d)", actor.state);
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_actor_control_handlers_nullpointers(void)
{
    PREPARE_ERROR(e);
    akgl_Actor actor;
    SDL_Event event;

    ATTEMPT {
	memset(&event, 0x00, sizeof(SDL_Event));
	memset(&actor, 0x00, sizeof(akgl_Actor));

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_on(NULL, &event), "left on, NULL actor");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_off(NULL, &event), "left off, NULL actor");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_on(NULL, &event), "right on, NULL actor");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_off(NULL, &event), "right off, NULL actor");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_on(NULL, &event), "up on, NULL actor");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_off(NULL, &event), "up off, NULL actor");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_on(NULL, &event), "down on, NULL actor");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_off(NULL, &event), "down off, NULL actor");

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_on(&actor, NULL), "left on, NULL event");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_off(&actor, NULL), "left off, NULL event");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_on(&actor, NULL), "right on, NULL event");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_off(&actor, NULL), "right off, NULL event");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_on(&actor, NULL), "up on, NULL event");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_off(&actor, NULL), "up off, NULL event");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_on(&actor, NULL), "down on, NULL event");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_off(&actor, NULL), "down off, NULL event");

	// A movement handler reads acceleration off the base character, so an
	// actor without one has to be reported rather than dereferenced.
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_left_on(&actor, &event),
			   "left on, actor with no base character");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_right_on(&actor, &event),
			   "right on, actor with no base character");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_up_on(&actor, &event),
			   "up on, actor with no base character");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_cmhf_down_on(&actor, &event),
			   "down on, actor with no base character");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_actor_automatic_face(void)
{
    PREPARE_ERROR(e);
    akgl_Actor actor;

    ATTEMPT {
	// With the flag off, facing is left entirely to the caller.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.movement_controls_face = false;
	actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
	TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face with the flag off");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
		    "automatic face changed facing while disabled (state %d)", actor.state);

	// With the flag on, facing follows movement.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.movement_controls_face = true;
	actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT);
	TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving left");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
		    "automatic face did not turn the actor left (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
		    "automatic face left the stale facing set (state %d)", actor.state);

	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.movement_controls_face = true;
	actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
	TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving right");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_RIGHT),
		    "automatic face did not turn the actor right (state %d)", actor.state);

	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.movement_controls_face = true;
	actor.state = AKGL_ACTOR_STATE_MOVING_UP;
	TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving up");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_UP),
		    "automatic face did not turn the actor up (state %d)", actor.state);

	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.movement_controls_face = true;
	actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
	TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving down");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_DOWN),
		    "automatic face did not turn the actor down (state %d)", actor.state);

	// Left wins over up when both are set, per the order of the checks.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.movement_controls_face = true;
	actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_UP);
	TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while moving diagonally");
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_FACE_LEFT),
		    "diagonal movement did not resolve to the left facing (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_UP),
		    "diagonal movement set two facings at once (state %d)", actor.state);

	// A stationary actor ends up facing nowhere.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.movement_controls_face = true;
	actor.state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP);
	TEST_EXPECT_OK(e, akgl_actor_automatic_face(&actor), "automatic face while stationary");
	TEST_ASSERT(e, AKGL_BITMASK_HASNOT(actor.state, AKGL_ACTOR_STATE_FACE_ALL),
		    "a stationary actor kept a facing bit (state %d)", actor.state);
	TEST_ASSERT(e, AKGL_BITMASK_HAS(actor.state, AKGL_ACTOR_STATE_ALIVE),
		    "automatic face cleared an unrelated state bit (state %d)", actor.state);

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_automatic_face(NULL),
			   "automatic face with a NULL actor");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_actor_logic_movement(void)
{
    PREPARE_ERROR(e);
    akgl_Actor actor;
    akgl_Character basechar;

    ATTEMPT {
	memset(&basechar, 0x00, sizeof(akgl_Character));
	basechar.ax = 3.0f; basechar.ay = 4.0f;
	basechar.sx = 30.0f; basechar.sy = 40.0f; basechar.sz = 50.0f;

	// Max speed is always refreshed from the base character.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.basechar = &basechar;
	TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while idle");
	TEST_ASSERT_FEQ(e, actor.sx, 30.0f, "sx copied as %f, expected 30", actor.sx);
	TEST_ASSERT_FEQ(e, actor.sy, 40.0f, "sy copied as %f, expected 40", actor.sy);
	TEST_ASSERT_FEQ(e, actor.sz, 50.0f, "sz copied as %f, expected 50", actor.sz);

	actor.state = AKGL_ACTOR_STATE_MOVING_LEFT;
	TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving left");
	TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "moving left set ax to %f, expected -3", actor.ax);

	actor.state = AKGL_ACTOR_STATE_MOVING_RIGHT;
	TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving right");
	TEST_ASSERT_FEQ(e, actor.ax, 3.0f, "moving right set ax to %f, expected 3", actor.ax);

	actor.state = AKGL_ACTOR_STATE_MOVING_UP;
	TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving up");
	TEST_ASSERT_FEQ(e, actor.ay, -4.0f, "moving up set ay to %f, expected -4", actor.ay);

	actor.state = AKGL_ACTOR_STATE_MOVING_DOWN;
	TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving down");
	TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "moving down set ay to %f, expected 4", actor.ay);

	// Both axes at once.
	actor.state = (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_DOWN);
	TEST_EXPECT_OK(e, akgl_actor_logic_movement(&actor, 1.0f), "movement logic while moving diagonally");
	TEST_ASSERT_FEQ(e, actor.ax, -3.0f, "diagonal movement set ax to %f, expected -3", actor.ax);
	TEST_ASSERT_FEQ(e, actor.ay, 4.0f, "diagonal movement set ay to %f, expected 4", actor.ay);

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(NULL, 1.0f),
			   "movement logic with a NULL actor");

	// Every value the movement logic writes is read off the base character,
	// so an actor without one has to be reported rather than dereferenced.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_movement(&actor, 1.0f),
			   "movement logic with no base character");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_actor_logic_changeframe(void)
{
    PREPARE_ERROR(e);
    akgl_Actor actor;
    akgl_Sprite sprite;

    ATTEMPT {
	memset(&sprite, 0x00, sizeof(akgl_Sprite));
	sprite.frames = 4;

	// Mid-animation, the frame simply advances.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.curSpriteFrameId = 1;
	TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing mid-animation");
	TEST_ASSERT(e, actor.curSpriteFrameId == 2,
		    "frame advanced to %d, expected 2", actor.curSpriteFrameId);

	// At the last frame without looping, it wraps to the start.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.curSpriteFrameId = 3;
	sprite.loop = false;
	sprite.loopReverse = false;
	TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a non-looping sprite");
	TEST_ASSERT(e, actor.curSpriteFrameId == 0,
		    "the last frame wrapped to %d, expected 0", actor.curSpriteFrameId);

	// A forward-looping sprite behaves the same way at the end.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.curSpriteFrameId = 3;
	sprite.loop = true;
	sprite.loopReverse = false;
	TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "wrapping a forward-looping sprite");
	TEST_ASSERT(e, actor.curSpriteFrameId == 0,
		    "the forward loop wrapped to %d, expected 0", actor.curSpriteFrameId);

	// A ping-pong sprite turns around at the end instead of wrapping.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	actor.curSpriteFrameId = 3;
	sprite.loop = true;
	sprite.loopReverse = true;
	TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "reversing at the end");
	TEST_ASSERT(e, actor.curSpriteReversing == true,
		    "the sprite did not enter its reverse phase at the last frame");
	TEST_ASSERT(e, actor.curSpriteFrameId == 2,
		    "reversing stepped to frame %d, expected 2", actor.curSpriteFrameId);

	// While reversing, the frame counts back down.
	TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "stepping back while reversing");
	TEST_ASSERT(e, actor.curSpriteFrameId == 1,
		    "reversing stepped to frame %d, expected 1", actor.curSpriteFrameId);

	// At frame zero it turns around again and resumes going forward.
	actor.curSpriteFrameId = 0;
	actor.curSpriteReversing = true;
	TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "turning around at frame zero");
	TEST_ASSERT(e, actor.curSpriteReversing == false,
		    "the sprite stayed in its reverse phase at frame zero");
	TEST_ASSERT(e, actor.curSpriteFrameId == 1,
		    "turning around stepped to frame %d, expected 1", actor.curSpriteFrameId);

	// A single-frame sprite has nowhere to advance to.
	memset(&actor, 0x00, sizeof(akgl_Actor));
	sprite.frames = 1;
	sprite.loop = false;
	sprite.loopReverse = false;
	TEST_EXPECT_OK(e, akgl_actor_logic_changeframe(&actor, &sprite, 0), "advancing a single-frame sprite");
	TEST_ASSERT(e, actor.curSpriteFrameId == 0,
		    "a single-frame sprite moved to frame %d, expected 0", actor.curSpriteFrameId);

	sprite.frames = 4;
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_logic_changeframe(NULL, &sprite, 0),
			   "changeframe with a NULL actor");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/** @brief Records calls made to the changeframe stub. */
static int changeframe_calls = 0;

/** @brief Changeframe stub that records its invocation without advancing. */
static akerr_ErrorContext *stub_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems)
{
    PREPARE_ERROR(e);
    changeframe_calls += 1;
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_actor_update(void)
{
    PREPARE_ERROR(e);
    akgl_Actor *actor = NULL;
    akgl_Character *basechar = NULL;
    akgl_Sprite *sprite = NULL;
    SDL_Time now = 0;

    ATTEMPT {
	CATCH(e, akgl_registry_init_actor());
	CATCH(e, akgl_registry_init_sprite());
	CATCH(e, akgl_registry_init_spritesheet());
	CATCH(e, akgl_registry_init_character());
	CATCH(e, akgl_heap_init());

	CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "updatable", AKGL_ACTOR_STATE_ALIVE));
	actor->changeframefunc = &stub_changeframe;

	// Long enough since the last frame change, so the sprite advances.
	SDL_GetCurrentTime(&now);
	sprite->speed = 1;
	actor->curSpriteFrameTimer = 0;
	changeframe_calls = 0;
	TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is due");
	TEST_ASSERT(e, changeframe_calls == 1,
		    "a due frame change fired %d times, expected 1", changeframe_calls);
	TEST_ASSERT(e, actor->curSpriteFrameTimer != 0,
		    "the frame timer was not restamped after a frame change");

	// Too soon since the last change, so nothing happens.
	SDL_GetCurrentTime(&now);
	sprite->speed = 1000000000;
	actor->curSpriteFrameTimer = now;
	changeframe_calls = 0;
	TEST_EXPECT_OK(e, akgl_actor_update(actor), "updating an actor whose frame is not due");
	TEST_ASSERT(e, changeframe_calls == 0,
		    "an early frame change fired %d times, expected 0", changeframe_calls);

	// An actor in a state with no sprite bound is skipped, not failed: the
	// missing binding is reported as AKERR_KEY and swallowed by update.
	actor->state = AKGL_ACTOR_STATE_DEAD;
	changeframe_calls = 0;
	TEST_EXPECT_OK(e, akgl_actor_update(actor),
		       "updating an actor whose state has no sprite");
	TEST_ASSERT(e, changeframe_calls == 0,
		    "an actor with no sprite for its state still changed frames");

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(NULL),
			   "updating a NULL actor");

	// An actor with no base character cannot resolve a sprite at all.
	actor->basechar = NULL;
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_actor_update(actor),
			   "updating an actor with no base character");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_actor_character_sprite_binding(void)
{
    PREPARE_ERROR(e);
    akgl_Actor *actor = NULL;
    akgl_Character *basechar = NULL;
    akgl_Sprite *sprite = NULL;
    akgl_Sprite *found = NULL;

    ATTEMPT {
	CATCH(e, akgl_registry_init_actor());
	CATCH(e, akgl_registry_init_sprite());
	CATCH(e, akgl_registry_init_spritesheet());
	CATCH(e, akgl_registry_init_character());
	CATCH(e, akgl_heap_init());

	CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "bound", AKGL_ACTOR_STATE_ALIVE));

	TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, &found),
		       "reading back a bound sprite");
	TEST_ASSERT(e, found == sprite, "the bound sprite came back as a different object");

	// A composite state is a distinct key from either of its components.
	TEST_EXPECT_STATUS(e, AKERR_KEY,
			   akgl_character_sprite_get(basechar,
						     (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
						     &found),
			   "reading a composite state that was never bound");
	TEST_EXPECT_STATUS(e, AKERR_KEY, akgl_character_sprite_get(basechar, 0, &found),
			   "reading state zero, which was never bound");

	// Binding a composite state makes it resolvable.
	TEST_EXPECT_OK(e, akgl_character_sprite_add(basechar, sprite,
						    (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP)),
		       "binding a sprite to a composite state");
	TEST_EXPECT_OK(e, akgl_character_sprite_get(basechar,
						    (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP),
						    &found),
		       "reading back the composite binding");
	TEST_ASSERT(e, found == sprite, "the composite binding resolved to a different sprite");

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_get(NULL, 0, &found),
			   "sprite_get with a NULL character");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER,
			   akgl_character_sprite_get(basechar, AKGL_ACTOR_STATE_ALIVE, NULL),
			   "sprite_get with a NULL destination");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(NULL, sprite, 0),
			   "sprite_add with a NULL character");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_character_sprite_add(basechar, NULL, 0),
			   "sprite_add with a NULL sprite");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_actor_sprite_sheet_coords(void)
{
    PREPARE_ERROR(e);
    akgl_Sprite sprite;
    SDL_FRect coords;

    ATTEMPT {
	memset(&sprite, 0x00, sizeof(akgl_Sprite));
	sprite.width = 32;
	sprite.height = 32;

	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_spritesheet_coords_for_frame(NULL, &coords, 0),
			   "sheet coords with a NULL sprite");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_spritesheet_coords_for_frame(&sprite, NULL, 0),
			   "sheet coords with a NULL rectangle");
	// The sprite has no sheet attached, so there is no texture to measure against.
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_spritesheet_coords_for_frame(&sprite, &coords, 0),
			   "sheet coords with a NULL spritesheet");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

/*
 * A render backend whose draw_texture records what it was asked to draw instead
 * of drawing it. akgl_actor_render's whole output is the rectangle it hands to
 * draw_texture, so that rectangle is the only thing there is to assert on.
 */
static akgl_RenderBackend testbackend;
static SDL_FRect lastdrawdest;
static SDL_FRect lastdrawsrc;
static int drawcalls = 0;

static akerr_ErrorContext *record_draw_texture(
    akgl_RenderBackend *self,
    SDL_Texture *texture,
    SDL_FRect *src,
    SDL_FRect *dest,
    double angle,
    SDL_FPoint *center,
    SDL_FlipMode flip)
{
    PREPARE_ERROR(errctx);

    if ( src != NULL ) {
	lastdrawsrc = *src;
    }
    if ( dest != NULL ) {
	lastdrawdest = *dest;
    }
    drawcalls += 1;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief akgl_actor_render must take the drawn height from the sprite's height.
 *
 * It read `curSprite->width` for both, so every actor was drawn square and a
 * non-square sprite was stretched or squashed. Invisible in the fixtures
 * because they are square, which is exactly why this test uses a sprite that
 * is not: 48 wide by 24 high, so a regression puts 48 in both.
 *
 * This is also the first test of akgl_actor_render at all -- everything else in
 * this file stubs renderfunc out.
 */
akerr_ErrorContext *test_actor_render_uses_sprite_height(void)
{
    PREPARE_ERROR(e);
    akgl_Actor *actor = NULL;
    akgl_Character *basechar = NULL;
    akgl_Sprite *sprite = NULL;
    SDL_Texture *sheettexture = NULL;

    ATTEMPT {
	CATCH(e, make_bound_actor(&actor, &basechar, &sprite, "renderactor", AKGL_ACTOR_STATE_ALIVE));

	// akgl_spritesheet_coords_for_frame reads sheet->texture->w, so the
	// sheet needs a real one even though nothing is rasterized.
	sheettexture = SDL_CreateTexture(
	    testbackend.sdl_renderer,
	    SDL_PIXELFORMAT_RGBA8888,
	    SDL_TEXTUREACCESS_TARGET,
	    96, 48);
	FAIL_ZERO_BREAK(e, sheettexture, AKGL_ERR_SDL, "%s", SDL_GetError());
	sprite->sheet->texture = sheettexture;

	sprite->width = 48;
	sprite->height = 24;
	sprite->frames = 2;
	actor->curSpriteFrameId = 0;
	actor->visible = true;
	actor->scale = 1.0f;
	actor->x = 0;
	actor->y = 0;

	akgl_camera->x = 0;
	akgl_camera->y = 0;
	akgl_camera->w = 640;
	akgl_camera->h = 480;

	drawcalls = 0;
	TEST_EXPECT_OK(e, akgl_actor_render(actor), "rendering a non-square actor");
	TEST_ASSERT(e, drawcalls == 1, "actor render made %d draw calls, expected 1", drawcalls);

	TEST_ASSERT_FEQ(e, lastdrawdest.w, 48.0f,
			"drawn width is %f, expected the sprite's 48", lastdrawdest.w);
	TEST_ASSERT_FEQ(e, lastdrawdest.h, 24.0f,
			"drawn height is %f, expected the sprite's 24 -- it used to take the width",
			lastdrawdest.h);

	// The source rectangle has always come from the sprite properly; assert
	// it too, so a regression cannot move the bug one line up.
	TEST_ASSERT_FEQ(e, lastdrawsrc.w, 48.0f, "source width is %f, expected 48", lastdrawsrc.w);
	TEST_ASSERT_FEQ(e, lastdrawsrc.h, 24.0f, "source height is %f, expected 24", lastdrawsrc.h);

	// Scale multiplies both, and must not collapse them back together.
	actor->scale = 2.0f;
	drawcalls = 0;
	TEST_EXPECT_OK(e, akgl_actor_render(actor), "rendering a scaled non-square actor");
	TEST_ASSERT_FEQ(e, lastdrawdest.w, 96.0f, "scaled width is %f, expected 96", lastdrawdest.w);
	TEST_ASSERT_FEQ(e, lastdrawdest.h, 48.0f, "scaled height is %f, expected 48", lastdrawdest.h);
    } CLEANUP {
	if ( sprite != NULL && sprite->sheet != NULL ) {
	    sprite->sheet->texture = NULL;
	}
	if ( sheettexture != NULL ) {
	    SDL_DestroyTexture(sheettexture);
	}
	if ( actor != NULL ) {
	    IGNORE(akgl_heap_release_actor(actor));
	}
	if ( basechar != NULL ) {
	    IGNORE(akgl_heap_release_character(basechar));
	}
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

int main(void)
{
    akgl_actor_updated = 0;
    akgl_actor_rendered = 0;
    UNHANDLED_ERROR_BEHAVIOR = UNHANDLED_ERROR_EXIT;
    PREPARE_ERROR(errctx);

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

    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	CATCH(errctx, akgl_registry_init_actor());
	CATCH(errctx, akgl_registry_init_sprite());
	CATCH(errctx, akgl_registry_init_spritesheet());
	CATCH(errctx, akgl_registry_init_character());

	// A renderer, so akgl_actor_render can be driven at all. The backend is
	// bound and then has its draw_texture replaced, which is the point:
	// what the function computes is only visible in what it hands over.
	if ( !SDL_Init(SDL_INIT_VIDEO) ) {
	    FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
	}
	memset(&testbackend, 0x00, sizeof(akgl_RenderBackend));
	if ( !SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_actor", 640, 480,
					  SDL_WINDOW_HIDDEN, &akgl_window, &testbackend.sdl_renderer) ) {
	    FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
	}
	CATCH(errctx, akgl_render_2d_bind(&testbackend));
	testbackend.draw_texture = &record_draw_texture;
	akgl_renderer = &testbackend;
	akgl_camera = &akgl_default_camera;

	CATCH(errctx, test_registry_actor_iterator_nullpointers());
	CATCH(errctx, test_registry_actor_iterator_missingactor());
	CATCH(errctx, test_registry_actor_iterator_updaterender());
	CATCH(errctx, test_akgl_actor_set_character());
	CATCH(errctx, test_actor_manage_children());

	CATCH(errctx, test_actor_control_handlers_on());
	CATCH(errctx, test_actor_control_handlers_off());
	CATCH(errctx, test_actor_control_handlers_nullpointers());
	CATCH(errctx, test_actor_automatic_face());
	CATCH(errctx, test_actor_logic_movement());
	CATCH(errctx, test_actor_logic_changeframe());
	CATCH(errctx, test_actor_update());
	CATCH(errctx, test_actor_character_sprite_binding());
	CATCH(errctx, test_actor_sprite_sheet_coords());
	CATCH(errctx, test_actor_render_uses_sprite_height());
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);

    return 0;
}
