/**
 * @file partition.c
 * @brief The broad phase, checked against brute force.
 *
 * A broad phase has one contract and it is asymmetric: it may report a pair that
 * does not overlap, and it may never miss one that does. Over-reporting costs a
 * narrowphase call; under-reporting is a wall an actor walks through, and it
 * fails silently and intermittently, only for the positions where the structure
 * happens to be wrong.
 *
 * So every query here is compared against a linear scan over the same proxies,
 * and the assertion is containment in one direction and not equality. That is
 * the only check that has any teeth: a broad phase can be tested against itself
 * all day and agree with itself all day.
 *
 * The suite is written as a table over partitioners so the same assertions run
 * against every implementation. A second implementation that is not held to the
 * first one's contract is not pluggable, it is just present.
 */

#include <math.h>
#include <string.h>

#include <akerror.h>

#include <akgl/collision.h>
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/tilemap.h>

#include "testutil.h"

/** @brief Every partitioner the factory knows, so the same tests run over each. */
static char *partitioners[] = { "grid", "bsp" };

/** @brief What a visit callback accumulates. */
typedef struct {
    int16_t seen[AKGL_MAX_HEAP_COLLISION_PROXY];
    int count;
} visit_record;

static akerr_ErrorContext *record_visit(akgl_CollisionProxy *proxy, void *data)
{
    visit_record *rec = (visit_record *)data;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, proxy, AKERR_NULLPOINTER, "NULL proxy in a visit");
    FAIL_ZERO_RETURN(errctx, rec, AKERR_NULLPOINTER, "NULL record in a visit");

    if ( rec->count < AKGL_MAX_HEAP_COLLISION_PROXY ) {
	rec->seen[rec->count] = (int16_t)(proxy - &akgl_heap_collision_proxies[0]);
	rec->count += 1;
    }
    SUCCEED_RETURN(errctx);
}

/** @brief How many cell entries are currently claimed from the pool. */
static int live_cells(void)
{
    int live = 0;
    int i = 0;

    for ( i = 0; i < AKGL_MAX_HEAP_COLLISION_CELL; i++ ) {
	if ( akgl_heap_collision_cells[i].refcount != 0 ) {
	    live += 1;
	}
    }
    return live;
}

/** @brief Was this proxy index reported? */
static bool sawit(visit_record *rec, int16_t idx)
{
    int i = 0;

    for ( i = 0; i < rec->count; i++ ) {
	if ( rec->seen[i] == idx ) {
	    return true;
	}
    }
    return false;
}

/** @brief Claim a proxy at a position and put it in the partitioner. */
static akerr_ErrorContext *spawn(akgl_CollisionWorld *world, akgl_CollisionShape *shape,
				 float32_t x, float32_t y, akgl_CollisionProxy **dest)
{
    PREPARE_ERROR(errctx);
    PASS(errctx, akgl_heap_next_collision_proxy(dest));
    PASS(errctx, akgl_collision_proxy_initialize(*dest, NULL, shape, x, y, 0.0f));
    PASS(errctx, world->partitioner.insert(&world->partitioner, *dest));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A query must report every proxy a linear scan would, and may report more.
 *
 * Positions are chosen to land on cell boundaries, span several cells, and sit
 * outside the grid entirely, because those are the three places an index is
 * wrong and a linear scan is not.
 */
akerr_ErrorContext *test_query_never_misses(char *name)
{
    akgl_CollisionWorld world;
    akgl_CollisionShape small;
    akgl_CollisionShape big;
    akgl_CollisionProxy *proxies[16];
    visit_record rec;
    SDL_FRect area;
    SDL_FRect small_body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
    SDL_FRect big_body = { .x = 0.0f, .y = 0.0f, .w = 96.0f, .h = 96.0f };
    float32_t spots[][2] = {
	{ 0.0f, 0.0f }, { 16.0f, 0.0f }, { 32.0f, 32.0f }, { 8.0f, 8.0f },
	{ 100.0f, 100.0f }, { 1000.0f, 40.0f }, { -40.0f, -40.0f }, { 15.9f, 15.9f }
    };
    int spotcount = (int)(sizeof(spots) / sizeof(spots[0]));
    bool missed = false;
    float32_t missx = 0.0f;
    float32_t missy = 0.0f;
    int i = 0;
    int q = 0;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
	CATCH(errctx, akgl_collision_shape_box(&small, &small_body, 0.0f));
	CATCH(errctx, akgl_collision_shape_box(&big, &big_body, 0.0f));

	for ( i = 0; i < spotcount; i++ ) {
	    CATCH(errctx, spawn(&world, &small, spots[i][0], spots[i][1], &proxies[i]));
	}
	// One deliberately large enough to spill onto the oversized chain, which
	// is a separate code path a query must not forget to walk.
	CATCH(errctx, spawn(&world, &big, 200.0f, 200.0f, &proxies[spotcount]));

	/*
	 * Sweep a query window across the world and compare against a linear
	 * scan every time. Recorded into a flag and asserted after the loops: a
	 * TEST_ASSERT in here reports by breaking, which would leave the loop
	 * rather than the ATTEMPT block and could not fail the test.
	 */
	for ( q = 0; q < 40; q++ ) {
	    area.x = (float32_t)((q * 13) % 300) - 50.0f;
	    area.y = (float32_t)((q * 7) % 300) - 50.0f;
	    area.w = 24.0f;
	    area.h = 24.0f;

	    memset(&rec, 0x00, sizeof(rec));
	    if ( world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
					 &record_visit, &rec) != NULL ) {
		missed = true;
		break;
	    }

	    for ( i = 0; i <= spotcount; i++ ) {
		if ( !SDL_HasRectIntersectionFloat(&proxies[i]->bounds, &area) ) {
		    continue;
		}
		if ( !sawit(&rec, (int16_t)(proxies[i] - &akgl_heap_collision_proxies[0])) ) {
		    missed = true;
		    missx = area.x;
		    missy = area.y;
		}
	    }
	}

	TEST_ASSERT(errctx, (missed == false),
		    "%s missed a proxy that overlaps the query at (%f, %f); a broad phase may "
		    "over-report and may never under-report", name, missx, missy);

	// A query is allowed to report a proxy more than the once, but must not:
	// a proxy spanning four cells appears on four chains.
	area.x = 0.0f;
	area.y = 0.0f;
	area.w = 300.0f;
	area.h = 300.0f;
	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
					      &record_visit, &rec));
	TEST_ASSERT(errctx, (rec.count <= (spotcount + 1)),
		    "%s reported %d proxies where at most %d exist; a proxy spanning several "
		    "cells is being reported once per cell", name, rec.count, spotcount + 1);
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The layer mask filters, and filters in the right direction.
 */
akerr_ErrorContext *test_query_respects_mask(char *name)
{
    akgl_CollisionWorld world;
    akgl_CollisionShape actor;
    akgl_CollisionShape scenery;
    akgl_CollisionProxy *a = NULL;
    akgl_CollisionProxy *b = NULL;
    visit_record rec;
    SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
    SDL_FRect area = { .x = -50.0f, .y = -50.0f, .w = 200.0f, .h = 200.0f };

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
	CATCH(errctx, akgl_collision_shape_box(&actor, &body, 0.0f));
	CATCH(errctx, akgl_collision_shape_box(&scenery, &body, 0.0f));
	scenery.layermask = AKGL_COLLISION_LAYER_STATIC;

	CATCH(errctx, spawn(&world, &actor, 0.0f, 0.0f, &a));
	CATCH(errctx, spawn(&world, &scenery, 32.0f, 0.0f, &b));

	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_STATIC,
					      &record_visit, &rec));
	TEST_ASSERT(errctx, (rec.count == 1), "%s reported %d proxies on the static layer, expected 1",
		    name, rec.count);
	TEST_ASSERT(errctx, sawit(&rec, (int16_t)(b - &akgl_heap_collision_proxies[0])),
		    "%s filtered out the static proxy and kept the actor", name);

	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_NONE,
					      &record_visit, &rec));
	TEST_ASSERT(errctx, (rec.count == 0), "%s reported %d proxies for an empty mask", name, rec.count);
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Moving a proxy has to update the index, and moving it nowhere has not to.
 *
 * The first half is the correctness claim: a stale entry in the old cell is the
 * classic incremental-structure bug, and it shows up as a collision with where
 * something used to be. The second half is the *performance* claim the whole
 * structure was chosen for, and it is asserted rather than assumed.
 */
akerr_ErrorContext *test_move_is_incremental(char *name)
{
    akgl_CollisionWorld world;
    akgl_CollisionShape shape;
    akgl_CollisionProxy *proxy = NULL;
    visit_record rec;
    SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 8.0f, .h = 8.0f };
    SDL_FRect here = { .x = 0.0f, .y = 0.0f, .w = 8.0f, .h = 8.0f };
    SDL_FRect there = { .x = 200.0f, .y = 200.0f, .w = 8.0f, .h = 8.0f };
    int16_t firstbefore = 0;
    int before = 0;
    int i = 0;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
	CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
	CATCH(errctx, spawn(&world, &shape, 0.0f, 0.0f, &proxy));

	// Move it a long way and re-query both ends.
	CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 200.0f, 200.0f, 0.0f));
	CATCH(errctx, world.partitioner.move(&world.partitioner, proxy));

	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.query(&world.partitioner, &here, AKGL_COLLISION_LAYER_ALL,
					      &record_visit, &rec));
	TEST_ASSERT(errctx, (rec.count == 0),
		    "%s still reports a proxy where it used to be; the old cell entry was not "
		    "removed, and a game would collide with a ghost", name);

	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.query(&world.partitioner, &there, AKGL_COLLISION_LAYER_ALL,
					      &record_visit, &rec));
	TEST_ASSERT(errctx, (rec.count == 1), "%s does not report the proxy where it now is", name);

	/*
	 * A move within the cells already occupied must touch nothing. That is
	 * the property the uniform grid was chosen for over a tree, so it is
	 * checked rather than trusted: the proxy's chain head is the cheapest
	 * observable proof that no entry was released and re-claimed.
	 */
	/*
	 * Only the grid claims to be incremental, and this is where that claim
	 * is checked rather than believed. The BSP is exempt by construction: it
	 * marks itself stale on any move and rebuilds, which is exactly the cost
	 * the grid was chosen to avoid and exactly why it is not the default.
	 */
	if ( strcmp(name, "grid") == 0 ) {
	    firstbefore = proxy->first;
	    CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 201.0f, 201.0f, 0.0f));
	    CATCH(errctx, world.partitioner.move(&world.partitioner, proxy));
	    TEST_ASSERT(errctx, (proxy->first == firstbefore),
			"%s re-celled a proxy that did not leave its cells; the structure is "
			"not incremental and the argument for choosing it does not hold", name);
	}

	/*
	 * Moving repeatedly must not accumulate index entries. A move that links
	 * into the new cells without unlinking the old ones answers *queries*
	 * correctly -- the bounds re-check filters the stale entries out -- so
	 * nothing above catches it. What it does instead is leak the cell pool
	 * until the grid stops working, on a timescale no unit test reaches by
	 * accident. Counting the pool is the only thing that sees it.
	 */
	before = live_cells();
	for ( i = 0; i < 200; i++ ) {
	    CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape,
						    (float32_t)((i * 37) % 400),
						    (float32_t)((i * 53) % 400), 0.0f));
	    CATCH(errctx, world.partitioner.move(&world.partitioner, proxy));
	}
	TEST_ASSERT(errctx, (live_cells() <= before),
		    "%s holds %d cell entries for one proxy after 200 moves, up from %d; the "
		    "old entries are not being unlinked and the pool is leaking",
		    name, live_cells(), before);

	// Removing is idempotent, because a caller tearing down should not have
	// to remember what it already dropped.
	CATCH(errctx, world.partitioner.remove(&world.partitioner, proxy));
	CATCH(errctx, world.partitioner.remove(&world.partitioner, proxy));
	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.query(&world.partitioner, &there, AKGL_COLLISION_LAYER_ALL,
					      &record_visit, &rec));
	TEST_ASSERT(errctx, (rec.count == 0), "%s still reports a removed proxy", name);
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/** @brief What each_pair accumulates. */
typedef struct {
    int count;
    int selfpairs;
} pair_record;

static akerr_ErrorContext *record_pair(akgl_CollisionProxy *a, akgl_CollisionProxy *b, void *data)
{
    pair_record *rec = (pair_record *)data;

    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(errctx, a, AKERR_NULLPOINTER, "NULL first proxy in a pair");
    FAIL_ZERO_RETURN(errctx, b, AKERR_NULLPOINTER, "NULL second proxy in a pair");
    FAIL_ZERO_RETURN(errctx, rec, AKERR_NULLPOINTER, "NULL record in a pair");

    if ( a == b ) {
	rec->selfpairs += 1;
    }
    rec->count += 1;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Every overlapping pair is reported, and none is reported twice.
 *
 * Two proxies sharing four cells is the case that produces four reports from a
 * naive implementation, and four reports means four impulses for one contact.
 */
akerr_ErrorContext *test_each_pair_is_once(char *name)
{
    akgl_CollisionWorld world;
    akgl_CollisionShape shape;
    akgl_CollisionProxy *proxies[4];
    pair_record rec;
    SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 40.0f, .h = 40.0f };
    int i = 0;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
	// 40 pixels on a 16 pixel grid: each of these covers nine cells, so every
	// pair among them shares several.
	CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));

	for ( i = 0; i < 4; i++ ) {
	    CATCH(errctx, spawn(&world, &shape, (float32_t)(i * 4), 0.0f, &proxies[i]));
	}

	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.each_pair(&world.partitioner, &record_pair, &rec));

	// Four mutually overlapping proxies are six distinct pairs.
	TEST_ASSERT(errctx, (rec.selfpairs == 0), "%s paired a proxy with itself %d times",
		    name, rec.selfpairs);
	TEST_ASSERT(errctx, (rec.count == 6),
		    "%s reported %d pairs among four mutually overlapping proxies, expected 6; "
		    "duplicates mean one contact resolved several times", name, rec.count);
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/** @brief An empty world answers nothing, and reset really empties it. */
akerr_ErrorContext *test_reset_and_empty(char *name)
{
    akgl_CollisionWorld world;
    akgl_CollisionShape shape;
    akgl_CollisionProxy *proxy = NULL;
    visit_record rec;
    SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
    SDL_FRect area = { .x = -100.0f, .y = -100.0f, .w = 400.0f, .h = 400.0f };

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));

	// The null hypothesis. A broken structure that reports nothing passes
	// every other test in this file, so this one is not a formality.
	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
					      &record_visit, &rec));
	TEST_ASSERT(errctx, (rec.count == 0), "%s reported %d proxies in an empty world",
		    name, rec.count);

	// Build the shape before spawning from it, not after. Spawning first
	// initializes the proxy from an uninitialized stack struct -- the sync
	// below overwrites it, so the test still passed, and valgrind still
	// reported eighteen uninitialised reads inside grid_cellrect.
	CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
	CATCH(errctx, spawn(&world, &shape, 0.0f, 0.0f, &proxy));
	CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 0.0f, 0.0f, 0.0f));
	CATCH(errctx, world.partitioner.move(&world.partitioner, proxy));

	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
					      &record_visit, &rec));
	TEST_ASSERT(errctx, (rec.count == 1), "%s lost the only proxy in the world", name);

	CATCH(errctx, world.partitioner.reset(&world.partitioner, &world));
	memset(&rec, 0x00, sizeof(rec));
	CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL,
					      &record_visit, &rec));
	TEST_ASSERT(errctx, (rec.count == 0), "%s still reports %d proxies after a reset",
		    name, rec.count);
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A small map with a floor, a pillar, a gap and a one-tile corridor.
 *
 * Synthesized rather than loaded: collision reads `data[]`, `tilewidth`,
 * `tileheight`, `width` and `height` and nothing else, so a fixture needs no
 * image, no tileset and no loader. `tests/perf_render.c` builds its map the same
 * way for the same reason.
 *
 * Every shape here is one the two example games actually hit -- a floor to rest
 * on, a pillar to walk into, a gap to fall through, a corridor that blocks both
 * axes at once.
 */
/*
 * Static, not on the stack. sizeof(akgl_Tilemap) is about 26 MB -- the layer and
 * tileset arrays are sized for the worst case the format allows -- so a local
 * one overflows the stack before the first assertion runs. tilemap.h says so and
 * this fixture was written wrong once anyway.
 */
static akgl_Tilemap fixture_map;

static void build_map(akgl_Tilemap *map)
{
    int x = 0;

    memset(map, 0x00, sizeof(akgl_Tilemap));
    map->tilewidth = 16;
    map->tileheight = 16;
    map->width = 20;
    map->height = 10;
    map->numlayers = 2;
    map->layers[0].type = AKGL_TILEMAP_LAYER_TYPE_TILES;
    map->layers[1].type = AKGL_TILEMAP_LAYER_TYPE_TILES;

    // Layer 0 is scenery and is not collidable. Layer 1 is terrain and is. If
    // the two were confused, an actor would collide with the background.
    for ( x = 0; x < map->width; x++ ) {
	map->layers[0].data[(2 * map->width) + x] = 99;
    }

    // A floor along row 9, with a two-tile gap at columns 5 and 6.
    for ( x = 0; x < map->width; x++ ) {
	if ( (x == 5) || (x == 6) ) {
	    continue;
	}
	map->layers[1].data[(9 * map->width) + x] = 1;
    }
    // A one-tile pillar at column 12, rows 7 and 8.
    map->layers[1].data[(7 * map->width) + 12] = 1;
    map->layers[1].data[(8 * map->width) + 12] = 1;

    map->collidablelayers = (1u << 1);
}

/**
 * @brief Solid tiles block, non-collidable layers do not, and off-map is open.
 */
akerr_ErrorContext *test_tiles_are_geometry(char *name)
{
    akgl_CollisionWorld world;
    akgl_Tilemap *map = &fixture_map;
    SDL_FRect box;
    bool solid = false;
    bool blocked = false;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
	build_map(map);
	CATCH(errctx, akgl_collision_bind_tilemap(&world, map));

	TEST_ASSERT(errctx, (world.cellwidth == 16.0f),
		    "binding a map left the cell width at %f rather than the tile width",
		    world.cellwidth);

	// On the floor.
	CATCH(errctx, akgl_collision_solid_at(&world, 8.0f, 152.0f, &solid));
	TEST_ASSERT(errctx, (solid == true), "%s does not see the floor tile at row 9", name);

	// In the gap in the floor.
	CATCH(errctx, akgl_collision_solid_at(&world, 88.0f, 152.0f, &solid));
	TEST_ASSERT(errctx, (solid == false), "%s reports the gap in the floor as solid", name);

	// On the scenery layer, which is not collidable. Getting this wrong
	// means an actor colliding with the background.
	CATCH(errctx, akgl_collision_solid_at(&world, 8.0f, 40.0f, &solid));
	TEST_ASSERT(errctx, (solid == false),
		    "%s treats a non-collidable layer as solid; `collidable` is being ignored", name);

	// Off the map is open, not solid. A pit has to be a pit.
	CATCH(errctx, akgl_collision_solid_at(&world, -100.0f, -100.0f, &solid));
	TEST_ASSERT(errctx, (solid == false), "%s reports off-map as solid", name);
	CATCH(errctx, akgl_collision_solid_at(&world, 8.0f, 10000.0f, &solid));
	TEST_ASSERT(errctx, (solid == false), "%s reports below the map as solid", name);

	// A box resting exactly on the floor must not read as inside it. Without
	// the epsilon on the far edge, an actor standing flush against geometry
	// reads as overlapping it and every move it tries looks blocked.
	box.x = 0.0f;
	box.y = 128.0f;
	box.w = 16.0f;
	box.h = 16.0f;
	CATCH(errctx, akgl_collision_box_blocked(&world, &box, AKGL_COLLISION_LAYER_ALL, &blocked));
	TEST_ASSERT(errctx, (blocked == false),
		    "%s reports a box resting exactly on the floor as inside it; an actor "
		    "standing on the ground would be unable to move", name);

	// One pixel lower and it is inside.
	box.y = 129.0f;
	CATCH(errctx, akgl_collision_box_blocked(&world, &box, AKGL_COLLISION_LAYER_ALL, &blocked));
	TEST_ASSERT(errctx, (blocked == true), "%s does not see a box overlapping the floor", name);

	// The pillar blocks horizontally.
	box.x = 190.0f;
	box.y = 112.0f;
	CATCH(errctx, akgl_collision_box_blocked(&world, &box, AKGL_COLLISION_LAYER_ALL, &blocked));
	TEST_ASSERT(errctx, (blocked == true), "%s does not see the pillar", name);

	// Detaching the map leaves nothing solid.
	CATCH(errctx, akgl_collision_bind_tilemap(&world, NULL));
	CATCH(errctx, akgl_collision_solid_at(&world, 8.0f, 152.0f, &solid));
	TEST_ASSERT(errctx, (solid == false), "%s still sees tiles after the map was detached", name);

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_collision_solid_at(NULL, 0.0f, 0.0f, &solid),
			   "a point query with no world");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_collision_box_blocked(&world, &box, 0, NULL),
			   "a box query with no destination");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Settling lifts a spawn out of the ground, and refuses when it cannot.
 */
akerr_ErrorContext *test_settle_lifts_a_spawn(char *name)
{
    akgl_CollisionWorld world;
    akgl_Tilemap *map = &fixture_map;
    akgl_CollisionShape shape;
    SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
    float32_t x = 0.0f;
    float32_t y = 0.0f;
    bool blocked = false;
    SDL_FRect box;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f));
	build_map(map);
	CATCH(errctx, akgl_collision_bind_tilemap(&world, map));
	CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));

	// Placed straddling the floor, which is what an editor does when an
	// object is dropped near a step.
	x = 32.0f;
	y = 140.0f;
	CATCH(errctx, akgl_collision_settle(&world, &shape, &x, &y, 0));
	TEST_ASSERT(errctx, (y < 140.0f), "%s did not lift a shape spawned inside the floor", name);

	CATCH(errctx, akgl_collision_shape_bounds(&shape, x, y, &box));
	CATCH(errctx, akgl_collision_box_blocked(&world, &box, AKGL_COLLISION_LAYER_STATIC, &blocked));
	TEST_ASSERT(errctx, (blocked == false), "%s settled a shape that is still inside geometry", name);

	// Already clear: settling must not move it at all.
	x = 32.0f;
	y = 40.0f;
	CATCH(errctx, akgl_collision_settle(&world, &shape, &x, &y, 0));
	TEST_ASSERT_FEQ(errctx, y, 40.0f, "%s moved a shape that was already clear, to %f", name, y);
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_world_and_factory(void)
{
    akgl_CollisionWorld world;
    akgl_Partitioner part;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_collision_world_init(&world, NULL, 16.0f, 16.0f));
	TEST_ASSERT(errctx, (strcmp(world.partitioner.name, "grid") == 0),
		    "a NULL partitioner name installed \"%s\" rather than the default",
		    world.partitioner.name);
	TEST_ASSERT(errctx, ((world.flags & AKGL_COLLISION_TEST_PLANAR) != 0),
		    "a new world does not force planar normals; every game today is 2D");

	CATCH(errctx, akgl_partitioner_factory(&part, "grid"));
	TEST_ASSERT(errctx, (part.query != NULL), "the grid installed no query");
	TEST_ASSERT(errctx, (part.move != NULL), "the grid installed no move");

	TEST_EXPECT_STATUS(errctx, AKERR_KEY, akgl_partitioner_factory(&part, "quadtree"),
			   "a partitioner nobody wrote");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_partitioner_factory(NULL, "grid"),
			   "the factory with no destination");
	TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_world_init(&world, NULL, 0.0f, 16.0f),
			   "a world with no cell width");
	TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_world_init(&world, NULL, 16.0f, -1.0f),
			   "a world with negative cell height");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_collision_world_init(NULL, NULL, 16.0f, 16.0f),
			   "a world with no destination");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    akerr_ErrorContext *inner = NULL;
    int i = 0;

    PREPARE_ERROR(errctx);

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

	/*
	 * Every partitioner is held to the same assertions. Collected rather
	 * than CATCHed inside the loop, because CATCH reports by breaking and a
	 * break here would leave the loop rather than the ATTEMPT block, so a
	 * failing partitioner would be silently skipped.
	 */
	for ( i = 0; i < (int)(sizeof(partitioners) / sizeof(partitioners[0])); i++ ) {
	    inner = test_query_never_misses(partitioners[i]);
	    if ( inner != NULL ) { break; }
	    inner = test_query_respects_mask(partitioners[i]);
	    if ( inner != NULL ) { break; }
	    inner = test_move_is_incremental(partitioners[i]);
	    if ( inner != NULL ) { break; }
	    inner = test_each_pair_is_once(partitioners[i]);
	    if ( inner != NULL ) { break; }
	    inner = test_reset_and_empty(partitioners[i]);
	    if ( inner != NULL ) { break; }
	    inner = test_tiles_are_geometry(partitioners[i]);
	    if ( inner != NULL ) { break; }
	    inner = test_settle_lifts_a_spawn(partitioners[i]);
	    if ( inner != NULL ) { break; }
	}
	CATCH(errctx, inner);
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);

    return 0;
}
