/**
 * @file testutil.h
 * @brief Shared assertion helpers and headless bootstrap for the libakgl test suites.
 *
 * The akerror ATTEMPT/CATCH/PROCESS/FINISH macros are verbose when what a test
 * wants to say is "this call must fail with exactly this status". These helpers
 * wrap that pattern.
 *
 * All of the TEST_* assertion macros expand to a `break` on failure, so they
 * must be used directly inside an ATTEMPT block. Inside a `for` or `while`
 * nested in an ATTEMPT they would break the inner loop instead; use
 * TEST_ASSERT_FLAG in that case and check the flag after the loop.
 */

#ifndef _AKGL_TESTUTIL_H_
#define _AKGL_TESTUTIL_H_

#include <math.h>
#include <stdlib.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/heap.h>

/**
 * @brief How many pool string slots are currently claimed.
 *
 * The string pool is 256 entries and every loader borrows scratch strings from
 * it, so "does this function give back what it took" is a question a test can
 * answer directly by counting either side of a call. A leak that would take
 * fifty map loads to show up in production shows up here on the first one.
 */
static inline int test_string_pool_used(void)
{
    int used = 0;
    int i = 0;

    for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
	if ( akgl_heap_strings[i].refcount != 0 ) {
	    used += 1;
	}
    }
    return used;
}

/** @brief Fail the enclosing ATTEMPT block unless @p cond holds. */
#define TEST_ASSERT(e, cond, ...)			\
    if ( ! (cond) ) {					\
	FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__);	\
    }

/**
 * @brief Run @p stmt and require that it reports exactly @p expected.
 *
 * Releases whatever context @p stmt returns, so a test can assert many failure
 * paths in a row without draining AKERR_ARRAY_ERROR. Pass 0 for @p expected to
 * require success.
 */
#define TEST_EXPECT_STATUS(e, expected, stmt, desc)		\
    {								\
	akerr_ErrorContext *__tec = (stmt);			\
	int __tst = ( __tec == NULL ) ? 0 : __tec->status;	\
	if ( __tec != NULL ) {					\
	    __tec->handled = true;				\
	    __tec = akerr_release_error(__tec);			\
	}							\
	if ( __tst != (expected) ) {				\
	    FAIL_BREAK(						\
		e,						\
		AKGL_ERR_BEHAVIOR,				\
		"%s: expected status %d (%s), got %d (%s)",	\
		desc,						\
		(int)(expected),				\
		akerr_name_for_status((int)(expected), NULL),	\
		__tst,						\
		akerr_name_for_status(__tst, NULL));		\
	}							\
    }

/** @brief Require that @p stmt succeeds, reporting @p desc if it does not. */
#define TEST_EXPECT_OK(e, stmt, desc)		\
    TEST_EXPECT_STATUS(e, 0, stmt, desc)

/**
 * @brief Require that @p stmt fails, without pinning which status it reports.
 *
 * For paths that are delegated to a dependency, where the exact status is that
 * dependency's business and asserting it would make the test brittle.
 */
#define TEST_EXPECT_ANY_ERROR(e, stmt, desc)				\
    {									\
	akerr_ErrorContext *__tec = (stmt);				\
	int __tst = ( __tec == NULL ) ? 0 : __tec->status;		\
	if ( __tec != NULL ) {						\
	    __tec->handled = true;					\
	    __tec = akerr_release_error(__tec);				\
	}								\
	if ( __tst == 0 ) {						\
	    FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, "%s: expected a failure, got success", desc); \
	}								\
    }

/** @brief Require that two floats agree to within AKGL_TEST_EPSILON. */
#define AKGL_TEST_EPSILON 0.0001f

#define TEST_ASSERT_FEQ(e, actual, expected, ...)			\
    if ( fabsf((float)(actual) - (float)(expected)) > AKGL_TEST_EPSILON ) { \
	FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__);			\
    }

/**
 * @brief Record a failure into a flag instead of breaking.
 *
 * For assertions inside a loop nested in an ATTEMPT block, where `break` would
 * only leave the loop.
 */
#define TEST_ASSERT_FLAG(flag, cond)		\
    if ( ! (cond) ) {				\
	(flag) = false;				\
    }

#endif // _AKGL_TESTUTIL_H_
