#include <SDL3/SDL.h>
#include <string.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>
#include <akgl/staticstring.h>
#include <akgl/util.h>

#include "testutil.h"

/**
 * @brief How many entries of AKERR_ARRAY_ERROR are currently held by somebody.
 *
 * The error contexts are a fixed pool exactly like the object pools: a context
 * is in use while its reference count is non-zero, and a function that finishes
 * without releasing one has leaked a slot out of AKERR_MAX_ARRAY_ERROR.
 */
static int live_error_contexts(void)
{
    int live = 0;
    int i = 0;

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

akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
{
    SDL_FRect testrect1 = {.x = 0, .y = 0, .w = 0, .h = 0};
    SDL_FRect testrect2 = {.x = 0, .y = 0, .w = 0, .h = 0};
    bool testcollide = false;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, NULL));
	FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, *, NULL) failed");
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_NULLPOINTER) {
	// noop
    } FINISH(errctx, true);

    ATTEMPT {
	CATCH(errctx, akgl_collide_rectangles(&testrect1, NULL, &testcollide));
	FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(*, NULL, *) failed");
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_NULLPOINTER) {
	// noop
    } FINISH(errctx, true);

    ATTEMPT {
	CATCH(errctx, akgl_collide_rectangles(NULL, &testrect2, &testcollide));
	FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, *, *) failed");
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_NULLPOINTER) {
	// noop
    } FINISH(errctx, true);

    ATTEMPT {
	CATCH(errctx, akgl_collide_rectangles(NULL, NULL, NULL));
	FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_rectangles(NULL, NULL, NULL) failed");
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_NULLPOINTER) {
	// noop
    } FINISH(errctx, true);

    ATTEMPT {
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}

/**
 * @brief The arrangements corner containment cannot see, and the ones it must keep.
 *
 * Eight corner-in-rectangle tests answer "do these overlap" correctly only when
 * one rectangle encloses a corner of the other. A tall thin rectangle crossing a
 * short wide one overlaps in a plus sign with no corner inside either, and eight
 * corner tests all report false. That was documented on akgl_collide_rectangles
 * as a known limitation rather than fixed, and there was no test for it.
 *
 * The three cases below the cross are the ones the rewrite could have broken
 * while fixing it: touching edges must keep counting as a collision, because the
 * corner form was inclusive on all four edges and the header promised it; full
 * containment must keep working; and a separation of less than one pixel must be
 * seen, which the old implementation could not do because it routed through the
 * since-removed akgl_Point and truncated float to int.
 */
akerr_ErrorContext *test_akgl_collide_rectangles_arrangements(void)
{
    SDL_FRect tall = { .x = 10.0f, .y = 0.0f, .w = 4.0f, .h = 40.0f };
    SDL_FRect wide = { .x = 0.0f, .y = 10.0f, .w = 40.0f, .h = 4.0f };
    SDL_FRect outer = { .x = 0.0f, .y = 0.0f, .w = 64.0f, .h = 64.0f };
    SDL_FRect inner = { .x = 16.0f, .y = 16.0f, .w = 8.0f, .h = 8.0f };
    SDL_FRect left = { .x = 0.0f, .y = 0.0f, .w = 10.0f, .h = 10.0f };
    SDL_FRect right = { .x = 10.0f, .y = 0.0f, .w = 10.0f, .h = 10.0f };
    SDL_FRect near1 = { .x = 0.0f, .y = 0.0f, .w = 10.9f, .h = 10.0f };
    SDL_FRect near2 = { .x = 10.5f, .y = 0.0f, .w = 10.0f, .h = 10.0f };
    bool collide = false;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	// The cross. Neither rectangle encloses a corner of the other, and they
	// plainly overlap in the middle.
	CATCH(errctx, akgl_collide_rectangles(&tall, &wide, &collide));
	TEST_ASSERT(errctx, (collide == true),
		    "a tall rectangle crossing a wide one was reported as not colliding");

	// The same pair the other way round. The test is symmetric and has to be.
	CATCH(errctx, akgl_collide_rectangles(&wide, &tall, &collide));
	TEST_ASSERT(errctx, (collide == true),
		    "the crossing pair was reported as not colliding with the arguments swapped");

	// Full containment, both orders.
	CATCH(errctx, akgl_collide_rectangles(&outer, &inner, &collide));
	TEST_ASSERT(errctx, (collide == true), "a contained rectangle was missed");
	CATCH(errctx, akgl_collide_rectangles(&inner, &outer, &collide));
	TEST_ASSERT(errctx, (collide == true), "a containing rectangle was missed");

	// A shared edge and nothing more. Touching counts; util.h and chapter 19
	// both say so, and a span test written with < rather than <= silently
	// changes that.
	CATCH(errctx, akgl_collide_rectangles(&left, &right, &collide));
	TEST_ASSERT(errctx, (collide == true),
		    "two rectangles sharing exactly one edge were reported as not colliding");

	// Overlapping by four tenths of a pixel. The corner tests truncated
	// float to int on the way in, so this read as a shared edge at 10.
	CATCH(errctx, akgl_collide_rectangles(&near1, &near2, &collide));
	TEST_ASSERT(errctx, (collide == true), "a sub-pixel overlap was missed");

	// Separated by four tenths of a pixel. Truncation read this as touching.
	near2.x = 11.3f;
	CATCH(errctx, akgl_collide_rectangles(&near1, &near2, &collide));
	TEST_ASSERT(errctx, (collide == false), "a sub-pixel gap was reported as a collision");

	// Disjoint on one axis only, which is the case a span test gets wrong
	// when it forgets to check both.
	tall.x = 100.0f;
	CATCH(errctx, akgl_collide_rectangles(&tall, &wide, &collide));
	TEST_ASSERT(errctx, (collide == false),
		    "rectangles separated on x were reported as colliding");
	tall.x = 10.0f;
	tall.y = 100.0f;
	CATCH(errctx, akgl_collide_rectangles(&tall, &wide, &collide));
	TEST_ASSERT(errctx, (collide == false),
		    "rectangles separated on y were reported as colliding");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
{
    SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32};
    SDL_FRect testrect2 = { .x = 30, .y = 30, .w = 40, .h = 40};
    bool testcollide = false;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	// Collision overlapping on the top left
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
	if ( testcollide == false ) {
	    FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
	}

	// Collision overlapping on the top right
	testrect1.x = 64;
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
	if ( testcollide == false ) {
	    FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
	}

	// Collision overlapping on the bottom left
	testrect1.x = 0;
	testrect1.y = 32;
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
	if ( testcollide == false ) {
	    FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
	}

	// Collision overlapping on the bottom right
	testrect1.x = 32;
	testrect1.y = 32;
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
	if ( testcollide == false ) {
	    FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
	}

	// Collision overlapping the top edge
	testrect1.x = 0;
	testrect1.y = 0;
	testrect1.w = 60;
	testrect1.h = 32;
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
	if ( testcollide == false ) {
	    FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
	}

	// Collision overlapping the left edge
	testrect1.x = 0;
	testrect1.y = 0;
	testrect1.w = 35;
	testrect1.h = 80;
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
	if ( testcollide == false ) {
	    FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
	}

	// Collision overlapping the right edge
	testrect1.x = 65;
	testrect1.y = 0;
	testrect1.w = 60;
	testrect1.h = 80;
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
	if ( testcollide == false ) {
	    FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
	}

	// Collision overlapping the bottom edge
	testrect1.x = 0;
	testrect1.y = 65;
	testrect1.w = 80;
	testrect1.h = 32;
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
	if ( testcollide == false ) {
	    FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
	}

	// Not colliding
	testrect1.x = 0;
	testrect1.y = 0;
	testrect1.w = 16;
	testrect1.h = 16;
	CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
	if ( testcollide == true ) {
	    FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
	}

    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}


/**
 * @brief Resolving a path through the root fallback must give its context back.
 *
 * akgl_path_relative tries the working directory first and falls back to
 * resolving against @p root when that reports ENOENT. That fallback used to be
 * taken by returning from inside the HANDLE block, which skips the
 * RELEASE_ERROR that FINISH ends with -- so every call down that branch leaked
 * one entry of AKERR_ARRAY_ERROR, and the 129th call aborted the whole process
 * with "Unable to pull an error context from the array!". A single map load
 * resolves several paths this way.
 *
 * The loop runs well past AKERR_MAX_ARRAY_ERROR on purpose: at the old
 * behaviour this test does not fail, it terminates the suite.
 */
/**
 * @brief akgl_compare_sdl_surfaces must check geometry before it memcmps.
 *
 * It compared `s1->pitch * s1->h` bytes out of both surfaces without looking at
 * the second one's dimensions, so a smaller s2 was read past its end rather
 * than reported as a mismatch. Benign in practice and immediately fatal under a
 * memory checker, which is the reason to fix it rather than leave it.
 */
akerr_ErrorContext *test_akgl_compare_sdl_surfaces_checks_geometry(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *big = NULL;
    SDL_Surface *small = NULL;
    SDL_Surface *twin = NULL;

    ATTEMPT {
	big = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGBA8888);
	twin = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGBA8888);
	small = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA8888);
	FAIL_ZERO_BREAK(errctx, big, AKGL_ERR_SDL, "%s", SDL_GetError());
	FAIL_ZERO_BREAK(errctx, twin, AKGL_ERR_SDL, "%s", SDL_GetError());
	FAIL_ZERO_BREAK(errctx, small, AKGL_ERR_SDL, "%s", SDL_GetError());

	FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(big, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError());
	FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(twin, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError());
	FAIL_ZERO_BREAK(errctx, SDL_FillSurfaceRect(small, NULL, 0), AKGL_ERR_SDL, "%s", SDL_GetError());

	TEST_EXPECT_OK(errctx, akgl_compare_sdl_surfaces(big, twin),
		       "comparing two identical surfaces");

	// The one that used to read 4 KiB past the end of an 8x8 surface.
	TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(big, small),
			   "comparing a 32x32 surface against an 8x8 one");
	TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(small, big),
			   "comparing an 8x8 surface against a 32x32 one");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_compare_sdl_surfaces(NULL, big),
			   "comparing a NULL first surface");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_compare_sdl_surfaces(big, NULL),
			   "comparing a NULL second surface");

	// Same dimensions, different format: the pixels are not comparable even
	// though the byte count might be.
	SDL_DestroySurface(small);
	small = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_RGB24);
	FAIL_ZERO_BREAK(errctx, small, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_compare_sdl_surfaces(big, small),
			   "comparing two surfaces of different pixel formats");
    } CLEANUP {
	if ( big != NULL ) {
	    SDL_DestroySurface(big);
	}
	if ( twin != NULL ) {
	    SDL_DestroySurface(twin);
	}
	if ( small != NULL ) {
	    SDL_DestroySurface(small);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_path_relative_releases_contexts(void)
{
    PREPARE_ERROR(errctx);
    akgl_String *dst = NULL;
    int before = 0;
    int after = 0;
    int i = 0;

    PASS(errctx, akgl_heap_init());
    PASS(errctx, akgl_heap_next_string(&dst));
    before = live_error_contexts();

    for ( i = 0; i < (AKERR_MAX_ARRAY_ERROR * 2); i++ ) {
	PASS(errctx, akgl_path_relative("assets", "testcharacter.json", dst));
    }
    after = live_error_contexts();

    ATTEMPT {
	if ( after != before ) {
	    FAIL_BREAK(
		errctx,
		AKGL_ERR_BEHAVIOR,
		"akgl_path_relative leaked %d error context(s) over %d root-fallback resolutions",
		(after - before),
		(AKERR_MAX_ARRAY_ERROR * 2));
	}
    } CLEANUP {
	IGNORE(akgl_heap_release_string(dst));
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    PREPARE_ERROR(errctx);
    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
	CATCH(errctx, test_akgl_collide_rectangles_logic());
	CATCH(errctx, test_akgl_collide_rectangles_arrangements());
	CATCH(errctx, test_akgl_compare_sdl_surfaces_checks_geometry());
	CATCH(errctx, test_akgl_path_relative_releases_contexts());
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}
