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

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

#include "testutil.h"

/*
 * This suite used to declare its own file-scope `window` and `renderer`, back
 * when the library's globals carried those bare names too. Both definitions had
 * external linkage and the same spelling, so the executable's preempted the
 * shared library's: akgl_sprite_load_json read the test's `SDL_Renderer *`
 * through an `akgl_RenderBackend *` and happened not to crash. That is the
 * collision the akgl_ prefix on exported globals exists to prevent, and it was
 * invisible until they were renamed. The suite now binds a real backend, the
 * way tests/sprite.c and tests/text.c do.
 */

akerr_ErrorContext *test_akgl_character_initialize()
{
    PREPARE_ERROR(errctx);
    akgl_Character *testchar = NULL;
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_character(&testchar));
	CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testchar", NULL),
	    AKERR_KEY,
	    "Character was not placed in the registry");
	FAIL_NONZERO_BREAK(
	    errctx,
	    strcmp((char *)&testchar->name, "testchar"),
	    AKERR_VALUE,
	    "Character was not named properly ('testchar' vs '%s')",
	    (char *)&testchar->name);
	FAIL_ZERO_BREAK(
	    errctx,
	    testchar->state_sprites,
	    AKERR_NULLPOINTER,
	    "Character state sprites map was not initialized");
    } CLEANUP {
	IGNORE(akgl_heap_release_character(testchar));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_character_sprite_mgmt()
{
    akgl_Character *testchar = NULL;
    akgl_Sprite *testsprite = NULL;
    akgl_Sprite *testsprite2 = NULL;
    akgl_Sprite *comparesprite = NULL;
    PREPARE_ERROR(errctx);
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_character(&testchar));
	CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
	testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testsprite,
	    AKERR_KEY,
	    "Sprite loaded from json but not in registry");
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
	testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testsprite2,
	    AKERR_KEY,
	    "Sprite 2 loaded from json but not in registry");
	CATCH(errctx, testchar->sprite_add(testchar, testsprite, AKGL_ACTOR_STATE_ALIVE));
	CATCH(errctx, testchar->sprite_add(testchar, testsprite2, AKGL_ACTOR_STATE_DEAD));
	CATCH(errctx, testchar->sprite_get(testchar, (AKGL_ACTOR_STATE_ALIVE), &comparesprite));
	FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT");
	CATCH(errctx, testchar->sprite_get(testchar, AKGL_ACTOR_STATE_DEAD, &comparesprite));
	FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");
    } CLEANUP {
	IGNORE(akgl_heap_release_sprite(testsprite));
	IGNORE(akgl_heap_release_sprite(testsprite2));
	IGNORE(akgl_heap_release_character(testchar));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_character_iterate_state_sprites()
{
    akgl_Character *testchar = NULL;
    akgl_Sprite *testsprite = NULL;
    akgl_Sprite *testsprite2 = NULL;
    akgl_Iterator opflags = {.flags = AKGL_ITERATOR_OP_RELEASE, .layerid = 0};

    PREPARE_ERROR(errctx);
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_character(&testchar));
	CATCH(errctx, akgl_character_initialize(testchar, "testchar"));
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
	testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testsprite,
	    AKERR_KEY,
	    "Sprite loaded from json but not in registry");
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
	testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testsprite2,
	    AKERR_KEY,
	    "Sprite 2 loaded from json but not in registry");
	CATCH(errctx, testchar->sprite_add(testchar, testsprite, AKGL_ACTOR_STATE_ALIVE));
	CATCH(errctx, testchar->sprite_add(testchar, testsprite2, AKGL_ACTOR_STATE_DEAD));

	SDL_EnumerateProperties(testchar->state_sprites, &akgl_character_state_sprites_iterate, &opflags);

	// This is called by akgl_heap_release_character so we should assume that our property map is being
	// deleted soon after this. So we don't care if the sprites have been deleted from the sprite_states.
	// We just want to know they've been released.
	FAIL_NONZERO_BREAK(
	    errctx,
	    (testsprite->refcount > 1),
	    AKERR_VALUE,
	    "akgl_heap_release_sprite not called for testsprite from iterator");
	FAIL_NONZERO_BREAK(
	    errctx,
	    (testsprite2->refcount > 1),
	    AKERR_VALUE,
	    "akgl_heap_release_sprite not called for testsprite from iterator");
    } CLEANUP {
	IGNORE(akgl_heap_release_sprite(testsprite));
	IGNORE(akgl_heap_release_sprite(testsprite2));
	IGNORE(akgl_heap_release_character(testchar));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_character_load_json()
{
    akgl_Character *testcharacter = NULL;
    akgl_Sprite *testsprite = NULL;
    akgl_Sprite *testsprite2 = NULL;
    akgl_Sprite *comparesprite = NULL;
    int tsrc = 0;
    int tsrc2 = 0;

    PREPARE_ERROR(errctx);
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_character(&testcharacter));
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
	testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testsprite,
	    AKERR_KEY,
	    "Sprite loaded from json but not in registry");
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
	testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testsprite2,
	    AKERR_KEY,
	    "Sprite 2 loaded from json but not in registry");

	CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
	testcharacter = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
	FAIL_ZERO_BREAK(
	    errctx,
	    testcharacter,
	    AKERR_KEY,
	    "Character loaded from json but not in registry");
	CATCH(errctx, testcharacter->sprite_get(testcharacter, (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT), &comparesprite));
	FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_ALIVE");
	CATCH(errctx, testcharacter->sprite_get(testcharacter, AKGL_ACTOR_STATE_DEAD, &comparesprite));
	FAIL_ZERO_BREAK(errctx, (comparesprite == testsprite2), AKERR_VALUE, "Wrong sprite for state AKGL_ACTOR_STATE_DEAD");

	FAIL_ZERO_BREAK(errctx, (testcharacter->ax != 0.200000003), AKERR_VALUE, "Wrong X acceleration for test character");
	FAIL_ZERO_BREAK(errctx, (testcharacter->ay != 0.200000003), AKERR_VALUE, "Wrong Y acceleration for test character");

	// Release our handles on the sprites so the character's heap_release can reduce them to 0
	CATCH(errctx, akgl_heap_release_sprite(testsprite));
	CATCH(errctx, akgl_heap_release_sprite(testsprite2));
	tsrc = testsprite->refcount;
	tsrc2 = testsprite2->refcount;
	CATCH(errctx, akgl_heap_release_character(testcharacter));
	FAIL_ZERO_BREAK(
	    errctx,
	    ((testsprite->refcount < tsrc) || (testsprite2->refcount < tsrc2)),
	    AKERR_VALUE,
	    "character did not reduce reference count of its child sprites when released");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Rebinding a state must release the sprite it displaces.
 *
 * akgl_character_sprite_add took a reference for every binding and wrote over
 * any existing entry without releasing the one it replaced, so a character that
 * rebinds a state while alive leaked a sprite slot per rebind. Releasing at
 * teardown does not cover it: the map only holds the last binding by then, so
 * the earlier ones are unreachable.
 *
 * The write itself was also unchecked, so a failure to record the binding was
 * reported as success -- with the reference already taken.
 */
akerr_ErrorContext *test_character_sprite_rebind_releases_displaced(void)
{
    PREPARE_ERROR(errctx);
    akgl_Character *testchar = NULL;
    akgl_Sprite *first = NULL;
    akgl_Sprite *second = NULL;
    akgl_Sprite *found = NULL;
    int firstbefore = 0;
    int secondbefore = 0;
    int i = 0;

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_character(&testchar));
	CATCH(errctx, akgl_character_initialize(testchar, "rebindchar"));
	// Real sprites out of the fixtures: akgl_sprite_initialize refuses a
	// NULL spritesheet, and a hand-built sprite would not be a sprite.
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
	CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
	first = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
	second = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
	FAIL_ZERO_BREAK(errctx, first, AKERR_KEY, "testsprite is not in the registry");
	FAIL_ZERO_BREAK(errctx, second, AKERR_KEY, "testsprite2 is not in the registry");

	firstbefore = first->refcount;
	secondbefore = second->refcount;

	CATCH(errctx, akgl_character_sprite_add(testchar, first, AKGL_ACTOR_STATE_ALIVE));
	TEST_ASSERT(errctx, first->refcount == (firstbefore + 1),
		    "binding a sprite did not take a reference (%d, expected %d)",
		    first->refcount, firstbefore + 1);

	// Rebind the same state to a different sprite.
	CATCH(errctx, akgl_character_sprite_add(testchar, second, AKGL_ACTOR_STATE_ALIVE));
	TEST_ASSERT(errctx, second->refcount == (secondbefore + 1),
		    "rebinding did not take a reference on the new sprite (%d, expected %d)",
		    second->refcount, secondbefore + 1);
	TEST_ASSERT(errctx, first->refcount == firstbefore,
		    "rebinding did not release the displaced sprite (%d, expected %d)",
		    first->refcount, firstbefore);

	CATCH(errctx, testchar->sprite_get(testchar, AKGL_ACTOR_STATE_ALIVE, &found));
	TEST_ASSERT(errctx, found == second, "the state is not bound to the new sprite");

	// Rebinding a state to the sprite already there must be a no-op on the
	// count rather than a release followed by a claim of the same slot.
	CATCH(errctx, akgl_character_sprite_add(testchar, second, AKGL_ACTOR_STATE_ALIVE));
	TEST_ASSERT(errctx, second->refcount == (secondbefore + 2),
		    "rebinding a state to the sprite it already held mishandled the count (%d)",
		    second->refcount);

	// And a long run of rebinds must not drift.
	for ( i = 0; i < 200; i++ ) {
	    CATCH(errctx, akgl_character_sprite_add(testchar, first, AKGL_ACTOR_STATE_DEAD));
	    CATCH(errctx, akgl_character_sprite_add(testchar, second, AKGL_ACTOR_STATE_DEAD));
	}
	TEST_ASSERT(errctx, first->refcount <= (firstbefore + 1),
		    "200 rebinds left the first sprite at refcount %d", first->refcount);
    } CLEANUP {
	if ( testchar != NULL ) {
	    IGNORE(akgl_heap_release_character(testchar));
	}
    } 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_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_character", 640, 480, SDL_WINDOW_HIDDEN, &akgl_window, &akgl_renderer->sdl_renderer)) {
	    FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
	}
	CATCH(errctx, akgl_render_2d_bind(akgl_renderer));

	CATCH(errctx, akgl_heap_init());
	CATCH(errctx, akgl_registry_init());
	CATCH(errctx, test_akgl_character_initialize());
	CATCH(errctx, test_character_sprite_mgmt());
	CATCH(errctx, test_character_iterate_state_sprites());
	CATCH(errctx, test_akgl_character_load_json());
	CATCH(errctx, test_character_sprite_rebind_releases_displaced());
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
    return 0;
}
