/**
 * @file renderer.c
 * @brief Unit tests for the 2D render backend's vtable and its entry points.
 *
 * None of this needs a display. akgl_render_2d_bind() installs the backend's
 * methods and nothing else, so it can be checked against a backend that has no
 * SDL_Renderer at all; the entry points that do draw are checked against a
 * software renderer under the dummy video driver, the same way tests/draw.c
 * does it.
 *
 * akgl_render_2d_init() is not covered here: it creates a window from the
 * property registry and writes the `camera` global, which is what the offscreen
 * harness described in issue #6 exists to make testable.
 */

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

#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/renderer.h>
#include <akgl/registry.h>

#include "testutil.h"

/** @brief Width and height of the offscreen target the drawing tests use. */
#define TEST_TARGET_SIZE 32

/** @brief A backend bound to a live software renderer, built by main(). */
static akgl_RenderBackend bound;

akerr_ErrorContext *test_render_bind2d(void)
{
    PREPARE_ERROR(errctx);
    akgl_RenderBackend backend;

    ATTEMPT {
	// The state a host is in when it owns its own window: a zeroed backend
	// with somebody else's SDL_Renderer in it. Binding must fill in the six
	// methods and leave that renderer alone -- creating a second window is
	// precisely what an embedded interpreter must not do.
	memset(&backend, 0x00, sizeof(akgl_RenderBackend));
	backend.sdl_renderer = (SDL_Renderer *)&bound;

	TEST_EXPECT_OK(errctx, akgl_render_2d_bind(&backend), "binding the 2D backend");

	TEST_ASSERT(errctx, backend.sdl_renderer == (SDL_Renderer *)&bound,
		    "binding replaced the caller's SDL_Renderer");
	TEST_ASSERT(errctx, backend.shutdown == &akgl_render_2d_shutdown,
		    "binding did not install shutdown");
	TEST_ASSERT(errctx, backend.frame_start == &akgl_render_2d_frame_start,
		    "binding did not install frame_start");
	TEST_ASSERT(errctx, backend.frame_end == &akgl_render_2d_frame_end,
		    "binding did not install frame_end");
	TEST_ASSERT(errctx, backend.draw_texture == &akgl_render_2d_draw_texture,
		    "binding did not install draw_texture");
	TEST_ASSERT(errctx, backend.draw_mesh == &akgl_render_2d_draw_mesh,
		    "binding did not install draw_mesh");
	TEST_ASSERT(errctx, backend.draw_world == &akgl_render_2d_draw_world,
		    "binding did not install draw_world");

	// Binding a backend that has no renderer yet is legal -- the two halves
	// are separable in both directions -- and leaves it NULL rather than
	// inventing one.
	memset(&backend, 0x00, sizeof(akgl_RenderBackend));
	TEST_EXPECT_OK(errctx, akgl_render_2d_bind(&backend),
		       "binding a backend with no SDL renderer");
	TEST_ASSERT(errctx, backend.sdl_renderer == NULL,
		    "binding invented an SDL_Renderer");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_bind(NULL),
			   "binding a NULL backend");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_render_backend_without_a_renderer(void)
{
    PREPARE_ERROR(errctx);
    akgl_RenderBackend empty;

    ATTEMPT {
	// Bound but never given an SDL_Renderer. Every entry point that draws
	// has to report that rather than dereference it.
	memset(&empty, 0x00, sizeof(akgl_RenderBackend));
	CATCH(errctx, akgl_render_2d_bind(&empty));

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_start(&empty),
			   "starting a frame on a backend with no renderer");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_end(&empty),
			   "ending a frame on a backend with no renderer");

	// And a NULL backend outright. frame_start and frame_end used to read
	// self->sdl_renderer with no check on self at all, which is a segfault
	// rather than an error, while draw_texture beside them checked it.
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_frame_start(NULL),
			   "starting a frame on a NULL backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_frame_end(NULL),
			   "ending a frame on a NULL backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_shutdown(NULL),
			   "shutting down a NULL backend");

	// Shutdown is the one that has nothing to release yet, so it succeeds.
	TEST_EXPECT_OK(errctx, empty.shutdown(&empty), "shutting down an unused backend");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_render_frames_and_textures(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *surface = NULL;
    SDL_Texture *texture = NULL;
    SDL_FRect dest;
    SDL_FPoint center;

    ATTEMPT {
	dest.x = 0.0f;
	dest.y = 0.0f;
	dest.w = 8.0f;
	dest.h = 8.0f;
	center.x = 4.0f;
	center.y = 4.0f;

	surface = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32);
	FAIL_ZERO_BREAK(errctx, surface, AKGL_ERR_SDL, "%s", SDL_GetError());
	texture = SDL_CreateTextureFromSurface(bound.sdl_renderer, surface);
	FAIL_ZERO_BREAK(errctx, texture, AKGL_ERR_SDL, "%s", SDL_GetError());

	TEST_EXPECT_OK(errctx, bound.frame_start(&bound), "starting a frame");
	TEST_EXPECT_OK(errctx,
		       bound.draw_texture(&bound, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE),
		       "blitting a texture unrotated");
	// A non-zero angle takes the rotated path, which is the one that
	// consults the pivot and the flip mode.
	TEST_EXPECT_OK(errctx,
		       bound.draw_texture(&bound, texture, NULL, &dest, 90.0, &center, SDL_FLIP_HORIZONTAL),
		       "blitting a texture rotated");
	TEST_EXPECT_OK(errctx, bound.frame_end(&bound), "ending a frame");

	// A rotation with no pivot is refused: SDL's "NULL means the centre of
	// dest" convention is not offered here, so a NULL is a caller mistake
	// rather than a default.
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   bound.draw_texture(&bound, texture, NULL, &dest, 90.0, NULL, SDL_FLIP_NONE),
			   "blitting rotated with no pivot");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_render_2d_draw_texture(NULL, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE),
			   "blitting through a NULL backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   bound.draw_texture(&bound, NULL, NULL, &dest, 0, NULL, SDL_FLIP_NONE),
			   "blitting a NULL texture");
    } CLEANUP {
	if ( texture != NULL ) {
	    SDL_DestroyTexture(texture);
	}
	if ( surface != NULL ) {
	    SDL_DestroySurface(surface);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_render_unimplemented_and_guards(void)
{
    PREPARE_ERROR(errctx);

    ATTEMPT {
	// The 3D hook is not a stub that quietly does nothing. It refuses, so a
	// caller that reaches it finds out on the first frame.
	TEST_EXPECT_STATUS(errctx, AKERR_API, bound.draw_mesh(&bound),
			   "drawing a mesh through the 2D backend");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_draw_world(NULL, NULL),
			   "drawing the world through a NULL backend");

	// Shutting the 2D backend down releases nothing today, and says so.
	TEST_EXPECT_OK(errctx, bound.shutdown(&bound), "shutting the backend down");
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief akgl_render_2d_init must give back its pooled strings when a parse fails.
 *
 * It reads game.screenwidth and game.screenheight into two pool strings and
 * used to release them only after both had parsed, so a non-numeric value
 * returned past both and leaked two of the pool's 256 entries -- every time a
 * host started with a bad configuration and retried.
 *
 * The window creation after the parse is what makes the success path need a
 * display, so this only drives the failure path. That is the one that leaked.
 */
akerr_ErrorContext *test_render_2d_init_releases_strings_on_failure(void)
{
    PREPARE_ERROR(errctx);
    akgl_RenderBackend backend;
    int baseline = 0;
    int i = 0;

    ATTEMPT {
	CATCH(errctx, akgl_registry_init_properties());
	memset(&backend, 0x00, sizeof(akgl_RenderBackend));

	CATCH(errctx, akgl_set_property("game.screenwidth", "not-a-number"));
	CATCH(errctx, akgl_set_property("game.screenheight", "480"));

	baseline = test_string_pool_used();
	for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
	    TEST_EXPECT_ANY_ERROR(errctx, akgl_render_2d_init(&backend),
				  "initializing a 2D renderer from an unparseable width");
	}
	TEST_ASSERT(errctx, test_string_pool_used() == baseline,
		    "%d failed initializations left %d pool strings claimed, expected %d",
		    (AKGL_MAX_HEAP_STRING * 2), test_string_pool_used(), baseline);

	// The second property is the one read after the first parse, so fail on
	// it too and check the other order.
	CATCH(errctx, akgl_set_property("game.screenwidth", "640"));
	CATCH(errctx, akgl_set_property("game.screenheight", "also-not-a-number"));
	baseline = test_string_pool_used();
	for ( i = 0; i < (AKGL_MAX_HEAP_STRING * 2); i++ ) {
	    TEST_EXPECT_ANY_ERROR(errctx, akgl_render_2d_init(&backend),
				  "initializing a 2D renderer from an unparseable height");
	}
	TEST_ASSERT(errctx, test_string_pool_used() == baseline,
		    "%d failed initializations left %d pool strings claimed, expected %d",
		    (AKGL_MAX_HEAP_STRING * 2), test_string_pool_used(), baseline);
    } CLEANUP {
    } 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());
	memset(&bound, 0x00, sizeof(akgl_RenderBackend));

	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_Init(SDL_INIT_VIDEO),
	    AKGL_ERR_SDL,
	    "Couldn't initialize SDL: %s",
	    SDL_GetError());
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_CreateWindowAndRenderer(
		"net/aklabs/libakgl/test_renderer",
		TEST_TARGET_SIZE,
		TEST_TARGET_SIZE,
		0,
		&akgl_window,
		&bound.sdl_renderer),
	    AKGL_ERR_SDL,
	    "Couldn't create window/renderer: %s",
	    SDL_GetError());
	// The host owns the window; libakgl only ever binds to it. This is the
	// arrangement akgl_render_2d_bind exists for.
	CATCH(errctx, akgl_render_2d_bind(&bound));

	CATCH(errctx, test_render_bind2d());
	CATCH(errctx, test_render_backend_without_a_renderer());
	CATCH(errctx, test_render_frames_and_textures());
	CATCH(errctx, test_render_unimplemented_and_guards());
	CATCH(errctx, test_render_2d_init_releases_strings_on_failure());
    } CLEANUP {
	SDL_Quit();
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}
