#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <jansson.h>

#include <akerror.h>
#include <akgl/util.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/tilemap.h>
#include <akgl/actor.h>
#include <akgl/game.h>
#include <akstdlib.h>
#include <akgl/json_helpers.h>

#include "testutil.h"

/**
 * @brief akgl_get_json_tilemap_property must give back its scratch strings on every path.
 *
 * It claims two pool strings per call -- one for the property name it is
 * comparing, one for the declared type -- and used to leave the block through a
 * `SUCCEED_RETURN` from inside `ATTEMPT` on the path where it *found* what it
 * was asked for. That returns past `CLEANUP`, so the ordinary, successful
 * lookup was the leaking one, at two of the pool's 256 entries a time. A map
 * load does this several times per layer.
 *
 * Three paths, run more times than the pool has entries: found, absent, and
 * present-but-wrong-type. Against the old code the found loop exhausts the pool
 * and comes back AKGL_ERR_HEAP long before it finishes.
 */
akerr_ErrorContext *test_tilemap_property_lookup_releases_strings(void)
{
    PREPARE_ERROR(errctx);
    json_t *jsondoc = NULL;
    json_error_t jsonerr;
    akgl_String *pathstr = NULL;
    int baseline = 0;
    int propnum = 0;
    int i = 0;

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&pathstr));
	snprintf(
	    (char *)&pathstr->data,
	    AKGL_MAX_STRING_LENGTH,
	    "%s%s",
	    SDL_GetBasePath(),
	    "assets/snippets/test_tilemap_get_json_tilemap_property.json"
	    );
	jsondoc = json_load_file((char *)&pathstr->data, 0, (json_error_t *)&jsonerr);
	FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);

	baseline = test_string_pool_used();

	// The success path. Twice the pool size, so a leak of even one entry
	// per call cannot finish.
	for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
	    CATCH(errctx, akgl_get_json_properties_integer(jsondoc, "state", &propnum));
	}
	TEST_ASSERT(
	    errctx,
	    test_string_pool_used() == baseline,
	    "%d successful property lookups left %d pool strings claimed, expected %d",
	    (AKGL_MAX_HEAP_STRING * 2),
	    test_string_pool_used(),
	    baseline);

	// The absent path, which reports AKERR_KEY from after FINISH.
	for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
	    TEST_EXPECT_STATUS(
		errctx,
		AKERR_KEY,
		akgl_get_json_properties_integer(jsondoc, "no_such_property", &propnum),
		"looking up a property the object does not have");
	}
	TEST_ASSERT(
	    errctx,
	    test_string_pool_used() == baseline,
	    "absent-property lookups left %d pool strings claimed, expected %d",
	    test_string_pool_used(),
	    baseline);

	// The wrong-type path, which fails from inside the ATTEMPT block with
	// both scratch strings already claimed.
	for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
	    TEST_EXPECT_STATUS(
		errctx,
		AKERR_TYPE,
		akgl_get_json_properties_integer(jsondoc, "character", &propnum),
		"reading a string property as an int");
	}
	TEST_ASSERT(
	    errctx,
	    test_string_pool_used() == baseline,
	    "wrong-type lookups left %d pool strings claimed, expected %d",
	    test_string_pool_used(),
	    baseline);
    } CLEANUP {
	if ( jsondoc != NULL ) {
	    json_decref(jsondoc);
	    jsondoc = NULL;
	}
	if ( pathstr != NULL ) {
	    IGNORE(akgl_heap_release_string(pathstr));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
{
    PREPARE_ERROR(errctx);
    json_t *jsondoc = NULL;
    json_error_t jsonerr;
    akgl_String *tmpstr = NULL;
    int propnum;

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	CATCH(errctx, akgl_heap_next_string(&tmpstr));
	snprintf(
	    (char *)&tmpstr->data,
	    AKGL_MAX_STRING_LENGTH,
	    "%s%s",
	    SDL_GetBasePath(),
	    "assets/snippets/test_tilemap_get_json_tilemap_property.json"
	    );
	jsondoc = json_load_file((char *)&tmpstr->data, 0, (json_error_t *)&jsonerr);
	FAIL_ZERO_BREAK(errctx, jsondoc, AKERR_NULLPOINTER, "Failure loading json fixture: %s", (char *)jsonerr.text);
	CATCH(
	    errctx,
	    akgl_get_json_properties_string(
		jsondoc,
		"character",
		&tmpstr
		)
	    );
	FAIL_NONZERO_BREAK(
	    errctx,
	    strcmp((char *)&tmpstr->data, "testcharacter"),
	    AKERR_VALUE,
	    "Incorrect value loaded from property `character` (`testcharacter` vs `%s`)",
	    (char *)&tmpstr->data
	    );
	CATCH(
	    errctx,
	    akgl_get_json_properties_integer(
		jsondoc,
		"state",
		&propnum
		)
	    );
	FAIL_NONZERO_BREAK(
	    errctx,
	    (propnum != 6),
	    AKERR_VALUE,
	    "Incorrect value loaded from property `state` (6 vs %d)",
	    propnum
	    );
    } CLEANUP {
	if ( tmpstr != NULL ) {
	    IGNORE(akgl_heap_release_string(tmpstr));
	}
	if ( jsondoc != NULL ) {
	    json_decref(jsondoc);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
{
    int comparison_values[8] = {
	0, // Tile 0 X
	0, // Tile 0 Y
	10, // Tile 1 X
	0, // Tile 1 Y
	0, // Tile 2 X
	10, // Tile 2 Y
	10, // Tile 2 X
	10 // Tile 2 Y
    };
    int *comparison_ptrs[8] = {
	&akgl_gamemap->tilesets[0].tile_offsets[0][0], // Tile 0 X
	&akgl_gamemap->tilesets[0].tile_offsets[0][1], // Tile 0 Y
	&akgl_gamemap->tilesets[0].tile_offsets[1][0], // Tile 1 X
	&akgl_gamemap->tilesets[0].tile_offsets[1][1], // Tile 0 Y
	&akgl_gamemap->tilesets[0].tile_offsets[2][0], // Tile 2 X
	&akgl_gamemap->tilesets[0].tile_offsets[2][1], // Tile 2 Y
	&akgl_gamemap->tilesets[0].tile_offsets[3][0], // Tile 3 X
	&akgl_gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
    };
    int i = 0;

    memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
    akgl_gamemap->tilesets[0].tilecount = 4;
    akgl_gamemap->tilesets[0].columns = 2;
    akgl_gamemap->tilesets[0].firstgid = 1;
    akgl_gamemap->tilesets[0].imageheight = 20;
    akgl_gamemap->tilesets[0].imagewidth = 20;
    akgl_gamemap->tilesets[0].tilecount = 4;
    akgl_gamemap->tilesets[0].tileheight = 10;
    akgl_gamemap->tilesets[0].tilewidth = 10;
    akgl_gamemap->tilesets[0].spacing = 0;
    akgl_gamemap->tilesets[0].margin = 0;

    PREPARE_ERROR(errctx);
    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	CATCH(errctx, akgl_tilemap_compute_tileset_offsets(akgl_gamemap, 0));
	// Tile 0 X offset
	for ( i = 0; i < 8; i++ ) {
	    FAIL_NONZERO_BREAK(
		errctx,
		(*comparison_ptrs[i] != comparison_values[i]),
		AKERR_VALUE,
		"Tile offset incorrectly calculated for index %d (%d vs %d)",
		i,
		*comparison_ptrs[i],
		comparison_values[i]
		);
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The loader must refuse a map that would overrun its fixed tables.
 *
 * Two loops indexed straight from the document. `curlayer->objects[j]` had no
 * check against AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, and `dest->tilesets[i]`
 * none against AKGL_TILEMAP_MAX_TILESETS. The object one is the reachable one:
 * 128 objects is not a large object layer, and akgl_TilemapObject is big, so
 * the 129th wrote well past the end of the layer.
 *
 * akgl_tilemap_load_layers already bounded its own loop and raised
 * AKERR_OUTOFBOUNDS, so the shape to copy was in the same file.
 */
/**
 * @brief Releasing a map twice must not free a texture twice.
 *
 * The layers loop tested `dest->layers[i].texture` and then destroyed
 * `dest->tilesets[i].texture` -- already destroyed by the loop above it. So
 * every tileset texture was freed twice on a single release, and no image
 * layer's texture was freed at all. Neither pointer was cleared either, so a
 * second release was a use-after-free on top of that.
 */
/**
 * @brief A load/release cycle must give back every pooled string it took.
 *
 * Measured before the fix by counting non-zero HEAP_STRING refcounts across
 * cycles: five per cycle, exactly, and akgl_tilemap_release gave none of them
 * back. The pool is 256 entries, so the 52nd map load in a process found it
 * empty -- and until the accessors were fixed, "empty" arrived as a segfault
 * rather than as AKGL_ERR_HEAP.
 *
 * Blast radius is every level transition, and a game that reloads a level on
 * death hits it sooner.
 */
akerr_ErrorContext *test_akgl_tilemap_load_releases_strings(void)
{
    akgl_String *pathstr = NULL;
    PREPARE_ERROR(errctx);
    int baseline = 0;
    int afterfirst = 0;
    int i = 0;

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	CATCH(errctx, akgl_heap_next_string(&pathstr));
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
		 SDL_GetBasePath(), "assets/testmap.tmj");

	baseline = test_string_pool_used();

	memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
	CATCH(errctx, akgl_tilemap_load((char *)&pathstr->data, akgl_gamemap));
	CATCH(errctx, akgl_tilemap_release(akgl_gamemap));
	afterfirst = test_string_pool_used();
	TEST_ASSERT(errctx, afterfirst == baseline,
		    "one load/release cycle left %d pool strings claimed, expected %d",
		    afterfirst, baseline);

	// Then enough cycles that a leak of even one string per load would run
	// the pool dry, so this cannot pass by rounding.
	for ( i = 0; i < 64; i++ ) {
	    memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
	    CATCH(errctx, akgl_tilemap_load((char *)&pathstr->data, akgl_gamemap));
	    CATCH(errctx, akgl_tilemap_release(akgl_gamemap));
	}
	TEST_ASSERT(errctx, test_string_pool_used() == baseline,
		    "64 load/release cycles left %d pool strings claimed, expected %d",
		    test_string_pool_used(), baseline);
    } CLEANUP {
	if ( pathstr != NULL ) {
	    IGNORE(akgl_heap_release_string(pathstr));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_tilemap_release_is_idempotent(void)
{
    akgl_String *pathstr = NULL;
    PREPARE_ERROR(errctx);
    int i = 0;

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));

	CATCH(errctx, akgl_heap_next_string(&pathstr));
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
		 SDL_GetBasePath(), "assets/testmap.tmj");
	CATCH(errctx, akgl_tilemap_load((char *)&pathstr->data, akgl_gamemap));
	TEST_ASSERT(errctx, akgl_gamemap->numtilesets > 0,
		    "the fixture map loaded no tilesets, so this test proves nothing");
	TEST_ASSERT(errctx, akgl_gamemap->tilesets[0].texture != NULL,
		    "the fixture map's first tileset has no texture");

	TEST_EXPECT_OK(errctx, akgl_tilemap_release(akgl_gamemap), "releasing a loaded map");
	for ( i = 0; i < AKGL_TILEMAP_MAX_TILESETS; i++ ) {
	    TEST_ASSERT(errctx, akgl_gamemap->tilesets[i].texture == NULL,
			"tileset %d's texture pointer survived release", i);
	}
	for ( i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
	    TEST_ASSERT(errctx, akgl_gamemap->layers[i].texture == NULL,
			"layer %d's texture pointer survived release", i);
	}

	// The one that used to be a use-after-free.
	TEST_EXPECT_OK(errctx, akgl_tilemap_release(akgl_gamemap), "releasing the same map again");
	TEST_EXPECT_OK(errctx, akgl_tilemap_release(akgl_gamemap), "releasing it a third time");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_tilemap_release(NULL),
			   "releasing a NULL map");
    } CLEANUP {
	if ( pathstr != NULL ) {
	    IGNORE(akgl_heap_release_string(pathstr));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_tilemap_load_bounds_fixed_tables(void)
{
    akgl_String *pathstr = NULL;
    PREPARE_ERROR(errctx);
    json_t *doc = NULL;
    json_error_t errdata;

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	CATCH(errctx, akgl_heap_next_string(&pathstr));

	// Exactly the maximum must still load.
	memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
		 SDL_GetBasePath(), "assets/snippets/test_tilemap_max_objects.json");
	doc = json_load_file((char *)&pathstr->data, 0, &errdata);
	FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "max-objects fixture: %s", (char *)&errdata.text);
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
	TEST_EXPECT_OK(errctx,
		       akgl_tilemap_load_layer_objects(akgl_gamemap, doc, 0, pathstr),
		       "loading an object layer with exactly the maximum objects");
	json_decref(doc);
	doc = NULL;

	// One more must be refused rather than written.
	memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
		 SDL_GetBasePath(), "assets/snippets/test_tilemap_too_many_objects.json");
	doc = json_load_file((char *)&pathstr->data, 0, &errdata);
	FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "too-many-objects fixture: %s", (char *)&errdata.text);
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_tilemap_load_layer_objects(akgl_gamemap, doc, 0, pathstr),
			   "loading an object layer with one object too many");
	json_decref(doc);
	doc = NULL;

	// And the tileset table.
	memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s",
		 SDL_GetBasePath(), "assets/snippets/test_tilemap_too_many_tilesets.json");
	doc = json_load_file((char *)&pathstr->data, 0, &errdata);
	FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "too-many-tilesets fixture: %s", (char *)&errdata.text);
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_tilemap_load_tilesets(akgl_gamemap, doc, pathstr),
			   "loading a map with one tileset too many");
	TEST_ASSERT(errctx, akgl_gamemap->numtilesets <= AKGL_TILEMAP_MAX_TILESETS,
		    "numtilesets reached %d, past the %d the table holds",
		    akgl_gamemap->numtilesets, AKGL_TILEMAP_MAX_TILESETS);
    } CLEANUP {
	if ( doc != NULL ) {
	    json_decref(doc);
	}
	if ( pathstr != NULL ) {
	    IGNORE(akgl_heap_release_string(pathstr));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_tilemap_load_layer_objects(void)
{
    akgl_String *pathstr;
    PREPARE_ERROR(errctx);
    json_t *doc = NULL;
    json_t *layers = NULL;
    json_t *objectlayer = NULL;
    json_error_t errdata;
    akgl_Actor *testactor;

    memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));
    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	CATCH(errctx, akgl_heap_next_string(&pathstr));
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
	doc = json_load_file((char *)&pathstr->data, 0, &errdata);
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
	FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
	CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
	CATCH(errctx, akgl_get_json_array_index_object(layers, 1, &objectlayer));
	CATCH(errctx, akgl_tilemap_load_layer_objects(akgl_gamemap, objectlayer, 1, pathstr));

	testactor = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testactor,
	    AKERR_NULLPOINTER,
	    "Test Actor was not loaded from the test map"
	    );
	if ( (testactor->basechar != SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL)) ||
	     (testactor->layer != 1) ||
	     (testactor->state != 6) ||
	     (testactor->visible != true) ||
	     (testactor->x != 16) ||
	     (testactor->y != 16) ) {
	    FAIL_BREAK(errctx, AKERR_VALUE, "Test actor was loaded with incorrect values (check gdb)");
	}
    } CLEANUP {
	if ( pathstr != NULL ) {
	    IGNORE(akgl_heap_release_string(pathstr));
	}
	if ( doc != NULL ) {
	    json_decref(doc);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
{
    akgl_String *pathstr;
    PREPARE_ERROR(errctx);
    json_t *doc = NULL;
    json_t *layers = NULL;
    json_t *tilelayer = NULL;
    json_t *tiledata = NULL;
    json_error_t errdata;

    memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	CATCH(errctx, akgl_heap_next_string(&pathstr));
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
	doc = json_load_file((char *)&pathstr->data, 0, &errdata);
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
	FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
	CATCH(errctx, akgl_get_json_array_value(doc, "layers", &layers));
	CATCH(errctx, akgl_get_json_array_index_object(layers, 0, &tilelayer));
	CATCH(errctx, akgl_get_json_array_value(tilelayer, "data", &tiledata));
	CATCH(errctx, akgl_tilemap_load_layer_tile(akgl_gamemap, tilelayer, 0, pathstr));
	if ( (akgl_gamemap->layers[0].data[0] != 1) ||
	     (akgl_gamemap->layers[0].data[1] != 2) ||
	     (akgl_gamemap->layers[0].data[2] != 3) ||
	     (akgl_gamemap->layers[0].data[3] != 4) ) {
	    FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
	}
    } CLEANUP {
	if ( pathstr != NULL ) {
	    IGNORE(akgl_heap_release_string(pathstr));
	}
	if ( doc != NULL ) {
	    json_decref(doc);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A layer image path too long for the field must say so, not load the wrong file.
 *
 * The join is `"%s/%s"` of a directory and an image name into a
 * #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE buffer, and both inputs are
 * #AKGL_MAX_STRING_LENGTH wide, so it can overflow. It used to be a raw
 * `snprintf` under a silenced `-Wformat-truncation`: the path was quietly cut
 * short and handed to `IMG_LoadTexture`, which failed with SDL's "couldn't
 * open" against a filename the caller never wrote -- the length problem
 * reported as a missing-file problem, pointing at the wrong thing.
 *
 * `aksl_snprintf` treats truncation as the error it is. The directory here is
 * filled to capacity so the join cannot fit whatever the image name is.
 */
akerr_ErrorContext *test_akgl_tilemap_layer_image_path_too_long(void)
{
    PREPARE_ERROR(errctx);
    akgl_String *dirname = NULL;
    json_t *layer = NULL;
    json_error_t errdata;

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&dirname));
	CATCH(errctx, aksl_memset((void *)&dirname->data, 'd', sizeof(dirname->data)));
	dirname->data[sizeof(dirname->data) - 1] = '\0';

	layer = json_loads("{\"image\": \"tiles.png\"}", 0, &errdata);
	FAIL_ZERO_BREAK(errctx, layer, AKERR_VALUE, "Failed to build the test layer: %s", errdata.text);

	TEST_EXPECT_STATUS(
	    errctx,
	    AKERR_OUTOFBOUNDS,
	    akgl_tilemap_load_layer_image(akgl_gamemap, layer, 0, dirname),
	    "joining an image name onto a directory that fills the field");
    } CLEANUP {
	if ( dirname != NULL ) {
	    IGNORE(akgl_heap_release_string(dirname));
	}
	if ( layer != NULL ) {
	    json_decref(layer);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
{
    akgl_String *pathstr;
    PREPARE_ERROR(errctx);
    json_t *doc = NULL;
    json_error_t errdata;
    int i = 0;

    memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	CATCH(errctx, akgl_heap_next_string(&pathstr));
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
	doc = json_load_file((char *)&pathstr->data, 0, &errdata);
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
	FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
	CATCH(errctx, akgl_tilemap_load_layers(akgl_gamemap, doc, pathstr));
	FAIL_NONZERO_BREAK(
	    errctx,
	    (akgl_gamemap->numlayers != 3),
	    AKERR_VALUE,
	    "Map layer count incorrect"
	    );
	for ( i = 0; i < akgl_gamemap->numlayers; i++ ) {
	    if ( (akgl_gamemap->layers[i].opacity != 1) ||
		 (akgl_gamemap->layers[i].visible != true) ||
		 (akgl_gamemap->layers[i].id != (i + 1)) ||
		 (akgl_gamemap->layers[i].x != 0) ||
		 (akgl_gamemap->layers[i].y != 0) ) {
		FAIL(errctx, AKERR_VALUE, "Map layer data loaded incorrectly (see gdb)");
		goto _test_akgl_tilemap_load_layers_cleanup;
	    }
	}
	// Layer 2 should have 1 object loaded
	if ( (akgl_gamemap->layers[1].objects[0].actorptr != SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "testactor", NULL)) ||
	     (akgl_gamemap->layers[1].objects[1].name[0] != '\0' ) ||
	     (akgl_gamemap->layers[1].objects[1].id != 0) ) {
	    FAIL_BREAK(errctx, AKERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)");
	}
	// Layer 1 and 3 should have no objects
	for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
	    if ( akgl_gamemap->layers[0].objects[i].id != 0 ) {
		FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
		goto _test_akgl_tilemap_load_layers_cleanup;
	    }
	}
	for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
	    if ( akgl_gamemap->layers[2].objects[i].id != 0 ) {
		FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
		goto _test_akgl_tilemap_load_layers_cleanup;
	    }
	}
	// Layers 1 and 3 should have tile data
	if ( (akgl_gamemap->layers[0].data[0] != 1) ||
	     (akgl_gamemap->layers[0].data[1] != 2) ||
	     (akgl_gamemap->layers[0].data[2] != 3) ||
	     (akgl_gamemap->layers[0].data[3] != 4) ||
	     (akgl_gamemap->layers[2].data[0] != 0) ||
	     (akgl_gamemap->layers[2].data[1] != 5) ||
	     (akgl_gamemap->layers[2].data[2] != 0) ||
	     (akgl_gamemap->layers[2].data[3] != 6)
	    ) {
	    FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect");
	}
    _test_akgl_tilemap_load_layers_cleanup:
    } CLEANUP {
	if ( pathstr != NULL ) {
	    IGNORE(akgl_heap_release_string(pathstr));
	}
	if ( doc != NULL ) {
	    json_decref(doc);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_tilemap_load_tilesets(void)
{
    akgl_String *pathstr = NULL;
    PREPARE_ERROR(errctx);
    json_t *doc = NULL;
    json_error_t errdata;
    SDL_Texture *image = NULL;

    memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	CATCH(errctx, akgl_heap_next_string(&pathstr));
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testmap.tmj");
	doc = json_load_file((char *)&pathstr->data, 0, &errdata);
	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets");
	FAIL_ZERO_BREAK(errctx, doc, AKERR_NULLPOINTER, "Failed to load testmap: %s", (char *)&errdata.text);
	CATCH(errctx, akgl_tilemap_load_tilesets(akgl_gamemap, doc, pathstr));
	FAIL_NONZERO_BREAK(errctx, (akgl_gamemap->numtilesets != 1), AKERR_VALUE, "Incorrect number of tilesets loaded for map");
	if ( (akgl_gamemap->tilesets[0].columns != 48 ) ||
	     (akgl_gamemap->tilesets[0].firstgid != 1) ||
	     (akgl_gamemap->tilesets[0].imageheight != 576) ||
	     (akgl_gamemap->tilesets[0].imagewidth != 768) ||
	     (akgl_gamemap->tilesets[0].margin != 0) ||
	     (akgl_gamemap->tilesets[0].spacing != 0) ||
	     (akgl_gamemap->tilesets[0].tilecount != 1728) ||
	     (akgl_gamemap->tilesets[0].tileheight != 16) ||
	     (akgl_gamemap->tilesets[0].tilewidth != 16) ) {
	    FAIL_BREAK(errctx, AKERR_VALUE, "Tileset loaded with incorrect values");
	}
	FAIL_NONZERO_BREAK(
	    errctx,
	    strcmp((char *)&akgl_gamemap->tilesets[0].name, "World_A1"),
	    AKERR_VALUE,
	    "Tileset loaded with incorrect name");

	snprintf((char *)&pathstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/World_A1.png");
	image = IMG_LoadTexture(akgl_renderer->sdl_renderer, (char *)&pathstr->data);
	FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");

	CATCH(
	    errctx,
	    akgl_render_and_compare(
		akgl_gamemap->tilesets[0].texture,
		image,
		0, 0, 768, 576,
		"test_akgl_tilemap_loaded_tileset.png")
	    );

    } CLEANUP {
	if ( pathstr != NULL ) {
	    IGNORE(akgl_heap_release_string(pathstr));
	}
	if ( doc != NULL ) {
	    json_decref(doc);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

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

    memset((void *)akgl_gamemap, 0x00, sizeof(akgl_Tilemap));

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	CATCH(errctx, akgl_tilemap_load("assets/testmap.tmj", akgl_gamemap));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

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

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}


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

    ATTEMPT {
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
    } CLEANUP {
    } 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");
    SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");

    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	akgl_gamemap = &akgl_default_gamemap;
	akgl_renderer = &akgl_default_renderer;
	SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");

	if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
	    FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
	}

	if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &akgl_window, &akgl_renderer->sdl_renderer)) {
	    FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
	}
	akgl_renderer->draw_texture = &akgl_render_2d_draw_texture;

	CATCH(errctx, akgl_registry_init());
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
	CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));

	CATCH(errctx, test_tilemap_akgl_get_json_tilemap_property());
	CATCH(errctx, test_tilemap_property_lookup_releases_strings());
	CATCH(errctx, test_akgl_tilemap_compute_tileset_offsets());
	CATCH(errctx, test_akgl_tilemap_load_layer_objects());
	CATCH(errctx, test_akgl_tilemap_load_layer_tile());
	CATCH(errctx, test_akgl_tilemap_layer_image_path_too_long());
	CATCH(errctx, test_akgl_tilemap_load_layers());
	CATCH(errctx, test_akgl_tilemap_load_tilesets());
	CATCH(errctx, test_akgl_tilemap_load_bounds_fixed_tables());
	CATCH(errctx, test_akgl_tilemap_release_is_idempotent());
	CATCH(errctx, test_akgl_tilemap_load_releases_strings());
	//CATCH(errctx, test_akgl_tilemap_load());
	//CATCH(errctx, test_akgl_tilemap_draw_tileset());
	//CATCH(errctx, test_akgl_tilemap_draw());
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);

}
