/**
 * @file collision_arena.c
 * @brief The bump allocator libccd allocates from, and the proof it is wired up.
 *
 * Two things have to be true and they are proved differently.
 *
 * **libccd allocates from the arena and not from the C library.** Proved by
 * `akgl_ccd_arena_used()` being non-zero after a query that allocated. Before
 * the redirect was moved out of CMake's COMPILE_DEFINITIONS -- which cannot
 * carry a function-like macro and dropped it without a word -- this read zero
 * while libccd happily called the real `realloc`, so it is not a formality.
 *
 * **No `free()` inside libccd reaches the C library.** Proved by the process
 * still being alive. Handing an arena pointer to the real `free(3)` is
 * undefined behaviour, and glibc aborts on it rather than tolerating it, so a
 * suite that runs to completion after a query that allocated has demonstrated
 * the interception. There is no balance to check: `akgl_ccd_arena_free` is a
 * no-op by design, because the whole arena is released at once.
 */

#include <stdio.h>
#include <string.h>

#include <akerror.h>

#include <akgl/collision_arena.h>
#include <akgl/error.h>

#include "testutil.h"

akerr_ErrorContext *test_arena_bump_and_reset(void)
{
    void *a = NULL;
    void *b = NULL;
    size_t used = 0;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	akgl_ccd_arena_reset();
	TEST_ASSERT(errctx, (akgl_ccd_arena_used() == 0), "a reset arena is not empty");

	a = akgl_ccd_arena_realloc(NULL, 64);
	TEST_ASSERT(errctx, (a != NULL), "a 64 byte allocation from an empty arena failed");
	used = akgl_ccd_arena_used();
	TEST_ASSERT(errctx, (used >= 64), "64 bytes were handed out but only %zu accounted for", used);

	b = akgl_ccd_arena_realloc(NULL, 64);
	TEST_ASSERT(errctx, (b != NULL), "a second allocation failed");
	TEST_ASSERT(errctx, (b != a), "two allocations returned the same block");
	TEST_ASSERT(errctx, (akgl_ccd_arena_used() > used), "a second allocation did not advance the arena");

	// Every block is aligned, because libccd allocates structures of doubles
	// and pointers and this allocator does not know which.
	TEST_ASSERT(errctx, ((((uintptr_t)a) % 16) == 0), "block a is not 16 byte aligned");
	TEST_ASSERT(errctx, ((((uintptr_t)b) % 16) == 0), "block b is not 16 byte aligned");

	// Freeing is a no-op by design: the whole arena goes at once.
	akgl_ccd_arena_free(a);
	akgl_ccd_arena_free(NULL);
	TEST_ASSERT(errctx, (akgl_ccd_arena_used() > 0), "free() released a block it should have ignored");

	akgl_ccd_arena_reset();
	TEST_ASSERT(errctx, (akgl_ccd_arena_used() == 0), "reset did not empty the arena");
    } CLEANUP {
	akgl_ccd_arena_reset();
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_arena_realloc_preserves_contents(void)
{
    unsigned char *block = NULL;
    unsigned char *grown = NULL;
    int i = 0;
    bool intact = true;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	akgl_ccd_arena_reset();
	block = (unsigned char *)akgl_ccd_arena_realloc(NULL, 32);
	TEST_ASSERT(errctx, (block != NULL), "the initial allocation failed");
	for ( i = 0; i < 32; i++ ) {
	    block[i] = (unsigned char)(i + 1);
	}

	grown = (unsigned char *)akgl_ccd_arena_realloc(block, 128);
	TEST_ASSERT(errctx, (grown != NULL), "growing a block failed");
	for ( i = 0; i < 32; i++ ) {
	    if ( grown[i] != (unsigned char)(i + 1) ) {
		intact = false;
	    }
	}
	TEST_ASSERT(errctx, (intact == true), "growing a block did not carry its contents across");

	// Shrinking copies only what fits, and must not read past the source.
	grown = (unsigned char *)akgl_ccd_arena_realloc(grown, 16);
	TEST_ASSERT(errctx, (grown != NULL), "shrinking a block failed");
	TEST_ASSERT(errctx, (grown[0] == 1), "shrinking a block lost its contents");

	// A zero-size request is not an allocation.
	TEST_ASSERT(errctx, (akgl_ccd_arena_realloc(NULL, 0) == NULL),
		    "a zero byte allocation returned a block");
    } CLEANUP {
	akgl_ccd_arena_reset();
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_arena_exhaustion_refuses(void)
{
    void *block = NULL;
    uint32_t before = 0;
    int i = 0;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	akgl_ccd_arena_reset();
	before = akgl_ccd_arena_exhausted();

	// A single request larger than the whole arena is refused rather than
	// satisfied by running off the end of the static array.
	TEST_ASSERT(errctx, (akgl_ccd_arena_realloc(NULL, AKGL_CCD_ARENA_BYTES * 2) == NULL),
		    "an allocation larger than the arena was satisfied");
	TEST_ASSERT(errctx, (akgl_ccd_arena_exhausted() > before),
		    "a refused allocation was not counted");

	// And filling it a block at a time ends in a refusal, not a corruption.
	akgl_ccd_arena_reset();
	for ( i = 0; i < ((AKGL_CCD_ARENA_BYTES / 1024) + 8); i++ ) {
	    block = akgl_ccd_arena_realloc(NULL, 1024);
	    if ( block == NULL ) {
		break;
	    }
	}
	TEST_ASSERT(errctx, (block == NULL), "the arena never refused, so it never filled");
	TEST_ASSERT(errctx, (akgl_ccd_arena_used() <= AKGL_CCD_ARENA_BYTES),
		    "the arena handed out %zu bytes of a %d byte pool",
		    akgl_ccd_arena_used(), AKGL_CCD_ARENA_BYTES);
    } CLEANUP {
	akgl_ccd_arena_reset();
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A real libccd query, and the balance that proves the shim is complete.
 *
 * `akgl_collision_arena_selftest` runs `ccdGJKPenetration`, which is the EPA
 * path and therefore the allocating one. If any of libccd's four bare `free()`
 * calls had escaped `src/ccd_arena_shim.h` it would have been handed an arena
 * pointer, and glibc would have aborted rather than returned -- so reaching the
 * assertion at all is half the proof. The other half is the balance: the arena
 * must be empty afterwards, because libccd frees its polytope before returning
 * and every one of those frees is a no-op that leaves `arena_used` alone only
 * if it reached ours.
 */
akerr_ErrorContext *test_arena_drives_a_real_query(void)
{
    bool hit = false;
    float32_t depth = 0.0f;
    size_t highwater = 0;
    akerr_ErrorContext *inner = NULL;
    int i = 0;

    PREPARE_ERROR(errctx);

    ATTEMPT {
	akgl_ccd_arena_reset();

	// Half-extent 1 each, centres 1 apart: overlapping by 1 on x.
	CATCH(errctx, akgl_collision_arena_selftest(1.0f, &hit, &depth));
	TEST_ASSERT(errctx, (hit == true), "two overlapping boxes were reported as not colliding");
	TEST_ASSERT_FEQ(errctx, depth, 1.0f, "overlapping boxes penetrate by %f, expected 1", depth);

	highwater = akgl_ccd_arena_highwater();
	TEST_ASSERT(errctx, (highwater > 0),
		    "a GJK/EPA query allocated nothing, so it did not reach the arena");
	/*
	 * Printed because it is the number AKGL_CCD_ARENA_BYTES is sized from,
	 * and because it is only meaningful before the exhaustion case
	 * deliberately fills the arena -- the high-water mark is never reset.
	 */
	printf("one GJK/EPA box pair: high-water %zu bytes of %d\n",
	       highwater, AKGL_CCD_ARENA_BYTES);

	/*
	 * The arena is what libccd allocated from. If the __CCD_ALLOC_MEMORY
	 * redirect were missing -- as it was, silently, while it lived in
	 * CMake's COMPILE_DEFINITIONS -- this would be zero and libccd would be
	 * calling the C library instead.
	 *
	 * That the process is still running is the other half: every free()
	 * inside libccd was handed an arena pointer, and glibc aborts when the
	 * real free(3) is given one it did not issue.
	 */
	TEST_ASSERT(errctx, (akgl_ccd_arena_used() > 0),
		    "a query allocated nothing from the arena, so libccd is not using it");

	// Boxes well apart do not collide, and still leave the arena balanced.
	CATCH(errctx, akgl_collision_arena_selftest(8.0f, &hit, &depth));
	TEST_ASSERT(errctx, (hit == false), "two separated boxes were reported as colliding");

	// Exactly touching. Degenerate configurations are where a narrowphase
	// divides by zero, and the libccd revision pinned here (v2.1) is the one
	// that fixed exactly that in ccdMPRPenetration.
	CATCH(errctx, akgl_collision_arena_selftest(2.0f, &hit, &depth));

	/*
	 * Each query resets the arena on entry, so a thousand of them in a row
	 * cost what one costs. Without that, a no-op free and a bump allocator
	 * would fill 64 KB in nine queries.
	 *
	 * The loop body cannot use CATCH: it reports by `break`ing, which would
	 * leave the loop rather than the ATTEMPT block and turn a failing query
	 * into a silently short run. Collect the failure and hand it over after.
	 */
	for ( i = 0; i < 64; i++ ) {
	    inner = akgl_collision_arena_selftest(1.0f, &hit, &depth);
	    if ( inner != NULL ) {
		break;
	    }
	}
	CATCH(errctx, inner);
	TEST_ASSERT(errctx, (akgl_ccd_arena_used() <= highwater),
		    "64 queries used %zu bytes where one used %zu; the arena is not being reset",
		    akgl_ccd_arena_used(), highwater);

	/*
	 * Starve a real query and require it to report. This is the path that
	 * matters and the one a unit test of the allocator cannot reach: libccd
	 * has to notice the NULL, unwind its half-built polytope, and return -2,
	 * and the caller has to turn that into AKGL_ERR_COLLISION rather than
	 * quietly reporting "no collision" -- which would be a floor an actor
	 * falls through, reported as success.
	 */
	akgl_ccd_arena_set_limit(32);
	TEST_EXPECT_STATUS(errctx, AKGL_ERR_COLLISION,
			   akgl_collision_arena_selftest(1.0f, &hit, &depth),
			   "a query against a starved arena");
	akgl_ccd_arena_set_limit(0);

	// And the full arena still works afterwards, so the limit is a knob and
	// not a one-way door.
	CATCH(errctx, akgl_collision_arena_selftest(1.0f, &hit, &depth));
	TEST_ASSERT(errctx, (hit == true), "the query failed after the arena was restored");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_collision_arena_selftest(1.0f, NULL, &depth),
			   "the selftest with a NULL hit flag");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_collision_arena_selftest(1.0f, &hit, NULL),
			   "the selftest with a NULL depth");
    } CLEANUP {
	akgl_ccd_arena_set_limit(0);
	akgl_ccd_arena_reset();
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	CATCH(errctx, test_arena_bump_and_reset());
	CATCH(errctx, test_arena_realloc_preserves_contents());
	/*
	 * The real query runs before the exhaustion case, so the high-water mark
	 * it prints is what a narrowphase query actually costs rather than what
	 * a test deliberately filling the arena costs.
	 */
	CATCH(errctx, test_arena_drives_a_real_query());
	CATCH(errctx, test_arena_exhaustion_refuses());
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);

    return 0;
}
