/**
 * @file heap.c
 * @brief Unit tests for the fixed-size object pools and their refcounting.
 *
 * The pools are process-wide arrays, so every test that fills one calls
 * akgl_heap_init() first and leaves the heap empty behind it.
 */

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

#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/sprite.h>
#include <akgl/registry.h>
#include <akgl/staticstring.h>

#include "testutil.h"

/**
 * @brief Reset every pool and the registries that reference their objects.
 *
 * The registries hold raw pointers into the pools, so clearing a pool without
 * clearing the registry would leave dangling entries for the next test.
 */
static akerr_ErrorContext *reset_all_heaps(void)
{
    PREPARE_ERROR(e);
    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());
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_heap_init_clears_every_pool(void)
{
    PREPARE_ERROR(e);
    bool clean = true;
    int i = 0;

    ATTEMPT {
	// Dirty every pool, then require that init scrubs all of them.
	for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
	    akgl_heap_actors[i].refcount = 5;
	}
	for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
	    akgl_heap_sprites[i].refcount = 5;
	}
	for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
	    akgl_heap_spritesheets[i].refcount = 5;
	}
	for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
	    akgl_heap_characters[i].refcount = 5;
	}
	for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
	    akgl_heap_strings[i].refcount = 5;
	}

	CATCH(e, akgl_heap_init());

	for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
	    TEST_ASSERT_FLAG(clean, akgl_heap_actors[i].refcount == 0);
	}
	TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the actor pool");

	for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
	    TEST_ASSERT_FLAG(clean, akgl_heap_sprites[i].refcount == 0);
	}
	TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the sprite pool");

	for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
	    TEST_ASSERT_FLAG(clean, akgl_heap_spritesheets[i].refcount == 0);
	}
	TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the spritesheet pool");

	for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
	    TEST_ASSERT_FLAG(clean, akgl_heap_characters[i].refcount == 0);
	}
	TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the character pool");

	for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
	    TEST_ASSERT_FLAG(clean, akgl_heap_strings[i].refcount == 0);
	}
	TEST_ASSERT(e, clean, "akgl_heap_init left a nonzero refcount in the string pool");

	// akgl_heap_init_actor clears only the actor pool.
	akgl_heap_actors[0].refcount = 9;
	akgl_heap_sprites[0].refcount = 9;
	CATCH(e, akgl_heap_init_actor());
	TEST_ASSERT(e, akgl_heap_actors[0].refcount == 0,
		    "akgl_heap_init_actor did not clear the actor pool");
	TEST_ASSERT(e, akgl_heap_sprites[0].refcount == 9,
		    "akgl_heap_init_actor cleared the sprite pool as well");
	akgl_heap_sprites[0].refcount = 0;
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_heap_next_string_refcounting(void)
{
    PREPARE_ERROR(e);
    akgl_String *first = NULL;
    akgl_String *second = NULL;

    ATTEMPT {
	CATCH(e, reset_all_heaps());

	// akgl_heap_next_string is the only acquire function that claims the slot
	// it hands out; the others leave refcount at zero for the caller to set.
	CATCH(e, akgl_heap_next_string(&first));
	TEST_ASSERT(e, first->refcount == 1,
		    "akgl_heap_next_string returned a slot with refcount %d, expected 1",
		    first->refcount);

	CATCH(e, akgl_heap_next_string(&second));
	TEST_ASSERT(e, second != first,
		    "akgl_heap_next_string handed out the same slot twice");

	CATCH(e, akgl_heap_release_string(first));
	CATCH(e, akgl_heap_release_string(second));
	TEST_ASSERT(e, first->refcount == 0,
		    "releasing a string left refcount at %d", first->refcount);
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_heap_exhaustion(void)
{
    PREPARE_ERROR(e);
    akgl_Actor *actor = NULL;
    akgl_Sprite *sprite = NULL;
    akgl_SpriteSheet *sheet = NULL;
    akgl_Character *basechar = NULL;
    akgl_String *str = NULL;
    int i = 0;

    ATTEMPT {
	// Actors: the acquire function does not claim the slot, so the test does.
	CATCH(e, reset_all_heaps());
	for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
	    akgl_heap_actors[i].refcount = 1;
	}
	TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_actor(&actor),
			   "akgl_heap_next_actor with every slot claimed");

	CATCH(e, reset_all_heaps());
	for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
	    akgl_heap_sprites[i].refcount = 1;
	}
	TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_sprite(&sprite),
			   "akgl_heap_next_sprite with every slot claimed");

	CATCH(e, reset_all_heaps());
	for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
	    akgl_heap_spritesheets[i].refcount = 1;
	}
	TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_spritesheet(&sheet),
			   "akgl_heap_next_spritesheet with every slot claimed");

	CATCH(e, reset_all_heaps());
	for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
	    akgl_heap_characters[i].refcount = 1;
	}
	TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_character(&basechar),
			   "akgl_heap_next_character with every slot claimed");

	// Strings claim their own slots, so draining the pool needs no help.
	CATCH(e, reset_all_heaps());
	for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
	    CATCH(e, akgl_heap_next_string(&str));
	}
	TEST_EXPECT_STATUS(e, AKGL_ERR_HEAP, akgl_heap_next_string(&str),
			   "akgl_heap_next_string with the pool drained");

	// A single release makes exactly one slot available again.
	CATCH(e, akgl_heap_release_string(&akgl_heap_strings[0]));
	TEST_EXPECT_OK(e, akgl_heap_next_string(&str),
		       "akgl_heap_next_string after freeing one slot");
	TEST_ASSERT(e, str == &akgl_heap_strings[0],
		    "the reclaimed string was not the slot that was released");

	CATCH(e, reset_all_heaps());
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_heap_release_refcounting(void)
{
    PREPARE_ERROR(e);
    akgl_Sprite *sprite = NULL;

    ATTEMPT {
	CATCH(e, reset_all_heaps());
	CATCH(e, akgl_heap_next_sprite(&sprite));

	// A shared object survives until the last reference goes away.
	sprite->refcount = 2;
	strncpy((char *)&sprite->name, "shared", AKGL_SPRITE_MAX_NAME_LENGTH - 1);
	CATCH(e, akgl_heap_release_sprite(sprite));
	TEST_ASSERT(e, sprite->refcount == 1,
		    "releasing a twice-referenced sprite left refcount %d, expected 1",
		    sprite->refcount);
	TEST_ASSERT(e, sprite->name[0] == 's',
		    "releasing a still-referenced sprite cleared its data");

	CATCH(e, akgl_heap_release_sprite(sprite));
	TEST_ASSERT(e, sprite->refcount == 0,
		    "the final release left refcount %d, expected 0", sprite->refcount);
	TEST_ASSERT(e, sprite->name[0] == 0x00,
		    "the final release did not clear the sprite");

	// Releasing an already-free object is clamped, not wrapped below zero.
	CATCH(e, akgl_heap_release_sprite(sprite));
	TEST_ASSERT(e, sprite->refcount == 0,
		    "over-releasing drove refcount to %d, expected a clamp at 0",
		    sprite->refcount);
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_heap_release_actor_children(void)
{
    PREPARE_ERROR(e);
    akgl_Actor *parent = NULL;
    akgl_Actor *child = NULL;
    char namebuf[AKGL_ACTOR_MAX_NAME_LENGTH];
    bool released = true;
    int i = 0;

    ATTEMPT {
	CATCH(e, reset_all_heaps());
	CATCH(e, akgl_heap_next_actor(&parent));
	CATCH(e, akgl_actor_initialize(parent, "parent"));

	// Fill every child slot, then release the parent once.
	for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
	    snprintf((char *)&namebuf, AKGL_ACTOR_MAX_NAME_LENGTH, "child%d", i);
	    CATCH(e, akgl_heap_next_actor(&child));
	    CATCH(e, akgl_actor_initialize(child, (char *)&namebuf));
	    CATCH(e, akgl_actor_add_child(parent, child));
	}

	CATCH(e, akgl_heap_release_actor(parent));
	TEST_ASSERT(e, parent->refcount == 0,
		    "releasing the parent left refcount %d", parent->refcount);

	// Each child was initialized to 1 and incremented to 2 by add_child, so
	// the recursive release should have brought every one of them back to 1.
	for ( i = 1; i <= AKGL_ACTOR_MAX_CHILDREN; i++ ) {
	    TEST_ASSERT_FLAG(released, akgl_heap_actors[i].refcount == 1);
	}
	TEST_ASSERT(e, released,
		    "releasing a parent did not decrement every child exactly once");

	CATCH(e, reset_all_heaps());
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_heap_release_clears_registry(void)
{
    PREPARE_ERROR(e);
    akgl_Actor *actor = NULL;
    akgl_Character *basechar = NULL;

    ATTEMPT {
	CATCH(e, reset_all_heaps());

	CATCH(e, akgl_heap_next_actor(&actor));
	CATCH(e, akgl_actor_initialize(actor, "registered"));
	TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "registered", NULL) != NULL,
		    "akgl_actor_initialize did not register the actor");
	CATCH(e, akgl_heap_release_actor(actor));
	TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "registered", NULL) == NULL,
		    "releasing an actor left a dangling registry entry");

	CATCH(e, akgl_heap_next_character(&basechar));
	CATCH(e, akgl_character_initialize(basechar, "regchar"));
	TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "regchar", NULL) != NULL,
		    "akgl_character_initialize did not register the character");
	CATCH(e, akgl_heap_release_character(basechar));
	TEST_ASSERT(e, SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "regchar", NULL) == NULL,
		    "releasing a character left a dangling registry entry");

	CATCH(e, reset_all_heaps());
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

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

    ATTEMPT {
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_actor(NULL),
			   "akgl_heap_release_actor(NULL)");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_sprite(NULL),
			   "akgl_heap_release_sprite(NULL)");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_spritesheet(NULL),
			   "akgl_heap_release_spritesheet(NULL)");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_character(NULL),
			   "akgl_heap_release_character(NULL)");
	TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_heap_release_string(NULL),
			   "akgl_heap_release_string(NULL)");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

akerr_ErrorContext *test_heap_release_spritesheet_texture(void)
{
    PREPARE_ERROR(e);
    akgl_SpriteSheet *sheet = NULL;

    ATTEMPT {
	CATCH(e, reset_all_heaps());
	CATCH(e, akgl_heap_next_spritesheet(&sheet));

	// A spritesheet with no texture must still release cleanly; the texture
	// branch is exercised by the sprite suite, which has a live renderer.
	sheet->refcount = 1;
	sheet->texture = NULL;
	CATCH(e, akgl_heap_release_spritesheet(sheet));
	TEST_ASSERT(e, sheet->refcount == 0,
		    "releasing a textureless spritesheet left refcount %d", sheet->refcount);
	TEST_ASSERT(e, sheet->texture == NULL,
		    "releasing a spritesheet left a non-NULL texture pointer");
    } CLEANUP {
    } PROCESS(e) {
    } FINISH(e, true);
    SUCCEED_RETURN(e);
}

int main(void)
{
    PREPARE_ERROR(errctx);

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

    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_registry_init());

	CATCH(errctx, test_heap_init_clears_every_pool());
	CATCH(errctx, test_heap_next_string_refcounting());
	CATCH(errctx, test_heap_exhaustion());
	CATCH(errctx, test_heap_release_refcounting());
	CATCH(errctx, test_heap_release_actor_children());
	CATCH(errctx, test_heap_release_clears_registry());
	CATCH(errctx, test_heap_release_nullpointers());
	CATCH(errctx, test_heap_release_spritesheet_texture());
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}
