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

#include "testutil.h"

void reset_string_heap(void);

akerr_ErrorContext *test_fresh_heap_gives_strings(void)
{
    akgl_String *ptr = NULL;

    PREPARE_ERROR(errctx);
    for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
	ATTEMPT {
	    CATCH(errctx, akgl_heap_next_string(&ptr));
	} CLEANUP {
	    reset_string_heap();
	} PROCESS(errctx) {
	} FINISH(errctx, true);
    }

    return 0;
}

akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void)
{
    akgl_String *ptr;
    PREPARE_ERROR(errctx);
    for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
	akgl_heap_strings[i].refcount = 1;
    }
    for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
	ATTEMPT {
	    CATCH(errctx, akgl_heap_next_string(&ptr));
	} CLEANUP {
	} PROCESS(errctx) {
	} HANDLE(errctx, AKERR_NULLPOINTER) {
	    return 0;
	} FINISH(errctx, true);
    }
    FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Expected AKERR_NULLPOINTER when accessing beyond string heap bounds");
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_string_heap_honors_refcount(void)
{
    akgl_String *firstptr = &akgl_heap_strings[0];
    akgl_String *secondptr = &akgl_heap_strings[1];
    akgl_String *testptr = NULL;
    PREPARE_ERROR(errctx);
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&testptr));
	if ( testptr != firstptr ) {
	    FAIL_RETURN(
		errctx,
		AKERR_VALUE,
		"Expected testptr to equal (akgl_heap_strings[0] = %p) but got %p",
		firstptr,
		testptr
		);
	}
	CATCH(errctx, akgl_string_initialize(testptr, NULL));
	if ( testptr->refcount == 0 ) {
	    FAIL_RETURN(errctx, AKERR_VALUE, "Expected string reference count to be nonzero but got 0");
	}
	if ( testptr != firstptr ) {
	    FAIL_RETURN(
		errctx,
		AKERR_VALUE,
		"Expected testptr to equal (akgl_heap_strings[1] = %p) but got %p",
		secondptr,
		testptr
		);
	}
	CATCH(errctx, akgl_heap_next_string(&testptr));
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_strcpy_to_all_strings_no_segfault(void)
{
    char copybuf[AKGL_MAX_STRING_LENGTH];
    akgl_String *ptr;
    memset((void *)&copybuf, 'a', AKGL_MAX_STRING_LENGTH);
    PREPARE_ERROR(errctx);
    ATTEMPT {
	for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
	    CATCH(errctx, akgl_heap_next_string(&ptr));
	    strncpy(ptr->data, (char *)&copybuf, AKGL_MAX_STRING_LENGTH);
	}
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_akgl_string_initialize(void)
{
    akgl_String *ptr;
    PREPARE_ERROR(errctx);
    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&ptr));
	CATCH(errctx, akgl_string_initialize(ptr, NULL));
	FAIL_NONZERO_BREAK(errctx, ptr->data[0], AKERR_VALUE, "Expected empty zero length string data");

	CATCH(errctx, akgl_heap_release_string(ptr));
	CATCH(errctx, akgl_heap_next_string(&ptr));
	CATCH(errctx, akgl_string_initialize(ptr, "Test value"));
	FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data);

	CATCH(errctx, akgl_heap_release_string(NULL));
	FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    SUCCEED_RETURN(errctx);
}


void reset_string_heap(void)
{
    for ( int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
	memset(&akgl_heap_strings[i], 0x00, sizeof(akgl_String));
    }
}

/**
 * @brief akgl_string_initialize must not write past the buffer it is zeroing.
 *
 * The `NULL` init path zeroed `sizeof(akgl_String)` bytes starting at `data`.
 * `data` begins after the `int refcount` in front of it, so that ran four bytes
 * past the end of the object -- straight onto the *next* pool slot's refcount,
 * which is the field the allocator uses to decide whether a slot is free.
 *
 * Claiming two adjacent slots and initializing the first is enough to catch it:
 * the second's refcount goes to zero and the pool believes it is free while the
 * caller is still holding it.
 */
akerr_ErrorContext *test_akgl_string_initialize_stays_in_bounds(void)
{
    PREPARE_ERROR(errctx);
    akgl_String *first = NULL;
    akgl_String *second = NULL;

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&first));
	CATCH(errctx, akgl_heap_next_string(&second));
	TEST_ASSERT(errctx, second == (first + 1),
		    "the pool did not hand out adjacent slots; this test needs them");

	// A sentinel the overrun would land on. akgl_heap_next_string has
	// already set it to 1; make it something an accidental write cannot
	// coincide with.
	second->refcount = 0x5A;

	CATCH(errctx, akgl_string_initialize(first, NULL));

	TEST_ASSERT(errctx, second->refcount == 0x5A,
		    "initializing a string wrote past its buffer: the next slot's "
		    "refcount is %d, expected %d",
		    second->refcount, 0x5A);
	TEST_ASSERT(errctx, first->refcount == 1,
		    "initializing a string left its own refcount at %d, expected 1",
		    first->refcount);
	TEST_ASSERT(errctx, first->data[0] == '\0',
		    "initializing a string with NULL did not zero its buffer");
	TEST_ASSERT(errctx, first->data[AKGL_MAX_STRING_LENGTH - 1] == '\0',
		    "initializing a string with NULL did not zero its last byte");
    } CLEANUP {
	if ( second != NULL ) {
	    second->refcount = 1;
	    IGNORE(akgl_heap_release_string(second));
	}
	if ( first != NULL ) {
	    IGNORE(akgl_heap_release_string(first));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief akgl_string_copy must refuse a count that would leave both buffers.
 *
 * Both slots are exactly AKGL_MAX_STRING_LENGTH bytes, so a larger count read
 * past the end of one and wrote past the end of the other. The header used to
 * document that as behaviour.
 */
akerr_ErrorContext *test_akgl_string_copy_bounds_its_count(void)
{
    PREPARE_ERROR(errctx);
    akgl_String *src = NULL;
    akgl_String *dest = NULL;

    ATTEMPT {
	CATCH(errctx, akgl_heap_next_string(&src));
	CATCH(errctx, akgl_heap_next_string(&dest));
	CATCH(errctx, akgl_string_initialize(src, "bounded"));
	CATCH(errctx, akgl_string_initialize(dest, NULL));

	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH + 1),
			   "copying one byte more than a pool string holds");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_string_copy(src, dest, -1),
			   "copying a negative number of bytes");

	// The boundary itself is legal: it is exactly the buffer.
	TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, AKGL_MAX_STRING_LENGTH),
		       "copying exactly a pool string's length");
	TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0,
		    "a full-length copy did not transfer the contents");

	// 0 still means "the whole buffer" rather than "nothing".
	CATCH(errctx, akgl_string_initialize(dest, NULL));
	TEST_EXPECT_OK(errctx, akgl_string_copy(src, dest, 0),
		       "copying with a count of zero");
	TEST_ASSERT(errctx, strcmp((char *)&dest->data, "bounded") == 0,
		    "a zero-count copy did not transfer the whole buffer");
    } CLEANUP {
	if ( dest != NULL ) {
	    IGNORE(akgl_heap_release_string(dest));
	}
	if ( src != NULL ) {
	    IGNORE(akgl_heap_release_string(src));
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

int main(void)
{

    PREPARE_ERROR(errctx);
    ATTEMPT {
	CATCH(errctx, akgl_error_init());
	printf("test_fresh_heap_gives_string ....\n");
	test_fresh_heap_gives_strings();
	reset_string_heap();
	printf("test_string_heap_error_when_no_strings_left ...\n");
	test_string_heap_error_when_no_strings_left();
	reset_string_heap();
	printf("test_string_heap_honors_refcount ...\n");
	test_string_heap_honors_refcount();
	reset_string_heap();
	printf("test_strcpy_to_all_strings_no_segfault ...\n");
	test_strcpy_to_all_strings_no_segfault();
	reset_string_heap();
	printf("test_akgl_string_initialize....\n");
	test_akgl_string_initialize();
	reset_string_heap();
	printf("test_akgl_string_initialize_stays_in_bounds ...\n");
	CATCH(errctx, test_akgl_string_initialize_stays_in_bounds());
	reset_string_heap();
	printf("test_akgl_string_copy_bounds_its_count ...\n");
	CATCH(errctx, test_akgl_string_copy_bounds_its_count());
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);

    return 0;
}
