/**
 * @file draw.c
 * @brief Unit tests for the immediate-mode drawing primitives.
 *
 * Everything here draws into a small software renderer under the dummy video
 * driver and then reads the target back with SDL_RenderReadPixels, so the
 * assertions are about pixels that actually changed rather than about SDL
 * having been called. No window is shown and no display is required.
 *
 * The target is deliberately tiny: at 64x64 a full readback is 16 KB, which
 * makes it cheap to read the whole thing back after every operation.
 */

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

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

#include "testutil.h"

/** @brief Width and height of the offscreen target every test draws into. */
#define TEST_TARGET_SIZE 64

/** @brief Opaque black, what each test clears the target to. */
static const SDL_Color testblack = { 0x00, 0x00, 0x00, 0xff };
/** @brief The color most tests draw with. */
static const SDL_Color testred = { 0xff, 0x00, 0x00, 0xff };
/** @brief A second color, for tests that need to tell two marks apart. */
static const SDL_Color testgreen = { 0x00, 0xff, 0x00, 0xff };

/** @brief Clear the whole target to opaque black. */
static akerr_ErrorContext *clear_target(void)
{
    PREPARE_ERROR(errctx);
    FAIL_ZERO_RETURN(
	errctx,
	SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    FAIL_ZERO_RETURN(
	errctx,
	SDL_RenderClear(akgl_renderer->sdl_renderer),
	AKGL_ERR_SDL,
	"%s",
	SDL_GetError());
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Report whether one pixel of @p shot carries @p color.
 *
 * Alpha is not compared: the render target's own alpha depends on the format
 * SDL picked for it, and none of these tests draw translucently.
 */
static bool pixel_is(SDL_Surface *shot, int x, int y, SDL_Color color)
{
    uint8_t r = 0;
    uint8_t g = 0;
    uint8_t b = 0;
    uint8_t a = 0;

    if ( shot == NULL ) {
	return false;
    }
    if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) {
	return false;
    }
    return ((r == color.r) && (g == color.g) && (b == color.b));
}

akerr_ErrorContext *test_draw_point(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;

    ATTEMPT {
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_point(akgl_renderer, 10.0f, 20.0f, testred),
		       "plotting one pixel");

	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 10, 20, testred),
		    "the plotted pixel at 10,20 is not the color it was drawn with");
	TEST_ASSERT(errctx, pixel_is(shot, 11, 20, testblack),
		    "plotting one pixel also changed its neighbour at 11,20");
	TEST_ASSERT(errctx, pixel_is(shot, 10, 21, testblack),
		    "plotting one pixel also changed the pixel below it");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_point(NULL, 0.0f, 0.0f, testred),
			   "plotting through a NULL backend");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_line(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    bool onthe_line = true;
    int i = 0;

    ATTEMPT {
	CATCH(errctx, clear_target());
	// A vertical line, so every pixel of it is known without reasoning
	// about how SDL rasterises a diagonal.
	TEST_EXPECT_OK(errctx, akgl_draw_line(akgl_renderer, 5.0f, 4.0f, 5.0f, 12.0f, testred),
		       "drawing a vertical line");

	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	for ( i = 4; i <= 12; i++ ) {
	    TEST_ASSERT_FLAG(onthe_line, pixel_is(shot, 5, i, testred));
	}
	TEST_ASSERT(errctx, onthe_line == true,
		    "the vertical line from 5,4 to 5,12 has a gap in it");
	TEST_ASSERT(errctx, pixel_is(shot, 5, 3, testblack),
		    "the line ran past its first endpoint");
	TEST_ASSERT(errctx, pixel_is(shot, 5, 13, testblack),
		    "the line ran past its second endpoint");
	TEST_ASSERT(errctx, pixel_is(shot, 6, 8, testblack),
		    "the line is wider than one pixel");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_line(NULL, 0.0f, 0.0f, 1.0f, 1.0f, testred),
			   "drawing a line through a NULL backend");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_rects(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    SDL_FRect box;

    ATTEMPT {
	box.x = 8.0f;
	box.y = 8.0f;
	box.w = 16.0f;
	box.h = 16.0f;

	// The outline touches the border and leaves the middle alone.
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_rect(akgl_renderer, &box, testred), "outlining a rectangle");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the outline is missing its top left corner");
	TEST_ASSERT(errctx, pixel_is(shot, 23, 23, testred),
		    "the outline is missing its bottom right corner");
	TEST_ASSERT(errctx, pixel_is(shot, 16, 8, testred), "the outline is missing its top edge");
	TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testblack), "the outline filled its interior");
	SDL_DestroySurface(shot);
	shot = NULL;

	// The filled form covers the interior as well.
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred), "filling a rectangle");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testred), "the fill left its interior empty");
	TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the fill missed its top left corner");
	TEST_ASSERT(errctx, pixel_is(shot, 24, 24, testblack),
		    "the fill ran one pixel past its bottom right corner");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(NULL, &box, testred),
			   "outlining through a NULL backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(akgl_renderer, NULL, testred),
			   "outlining a NULL rectangle");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(NULL, &box, testred),
			   "filling through a NULL backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(akgl_renderer, NULL, testred),
			   "filling a NULL rectangle");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_circle(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    bool symmetric = true;
    int x = 0;
    int y = 0;

    ATTEMPT {
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_circle(akgl_renderer, 32.0f, 32.0f, 10.0f, testred),
		       "drawing a circle");

	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	// The four axis points are exact for any correct midpoint circle.
	TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testred), "the circle is missing its rightmost pixel");
	TEST_ASSERT(errctx, pixel_is(shot, 22, 32, testred), "the circle is missing its leftmost pixel");
	TEST_ASSERT(errctx, pixel_is(shot, 32, 42, testred), "the circle is missing its bottom pixel");
	TEST_ASSERT(errctx, pixel_is(shot, 32, 22, testred), "the circle is missing its top pixel");
	// It is an outline, not a disc.
	TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack), "the circle filled its center");
	// ...and nothing lands outside the radius.
	TEST_ASSERT(errctx, pixel_is(shot, 43, 32, testred) == false,
		    "the circle drew a pixel one past its radius");

	// Every plotted pixel has a mirror in the other three quadrants. The
	// circle is drawn one octant at a time and reflected seven ways, so a
	// sign error in any single reflection breaks this and nothing else --
	// the four axis points above stay put either way.
	for ( y = 22; y <= 42; y++ ) {
	    for ( x = 22; x <= 42; x++ ) {
		if ( !pixel_is(shot, x, y, testred) ) {
		    continue;
		}
		TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, y, testred));
		TEST_ASSERT_FLAG(symmetric, pixel_is(shot, x, 64 - y, testred));
		TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, 64 - y, testred));
	    }
	}
	TEST_ASSERT(errctx, symmetric == true,
		    "the circle is not symmetric about its center; an octant is reflected wrong");
	SDL_DestroySurface(shot);
	shot = NULL;

	// A zero radius is the degenerate case, not an error: one pixel.
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_circle(akgl_renderer, 5.0f, 5.0f, 0.0f, testred),
		       "drawing a circle of radius zero");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 5, 5, testred),
		    "a circle of radius zero did not plot its center");

	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_draw_circle(akgl_renderer, 5.0f, 5.0f, -1.0f, testred),
			   "drawing a circle of negative radius");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_circle(NULL, 5.0f, 5.0f, 4.0f, testred),
			   "drawing a circle through a NULL backend");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_flood_fill(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    SDL_FRect box;

    ATTEMPT {
	// A red box outline on black. Filling inside it must stay inside it,
	// which is the whole contract of PAINT.
	box.x = 10.0f;
	box.y = 10.0f;
	box.w = 20.0f;
	box.h = 20.0f;
	CATCH(errctx, clear_target());
	CATCH(errctx, akgl_draw_rect(akgl_renderer, &box, testred));

	TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 20, 20, testgreen),
		       "flooding the inside of a box");

	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen), "the seed pixel was not filled");
	TEST_ASSERT(errctx, pixel_is(shot, 11, 11, testgreen),
		    "the fill did not reach the top left of the interior");
	TEST_ASSERT(errctx, pixel_is(shot, 28, 28, testgreen),
		    "the fill did not reach the bottom right of the interior");
	TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred), "the fill overwrote the boundary");
	TEST_ASSERT(errctx, pixel_is(shot, 20, 10, testred), "the fill overwrote the top edge");
	TEST_ASSERT(errctx, pixel_is(shot, 20, 5, testblack), "the fill leaked outside the box");
	TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack),
		    "the fill leaked into the rest of the target");
	SDL_DestroySurface(shot);
	shot = NULL;

	// Filling a region that is already the requested color changes nothing
	// and is not an error.
	TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 20, 20, testgreen),
		       "flooding a region that is already that color");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen),
		    "refilling a region disturbed it");
	TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
		    "refilling a region disturbed its boundary");
	SDL_DestroySurface(shot);
	shot = NULL;

	// Flooding the outside reaches every pixel that is not the box.
	TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(akgl_renderer, 0, 0, testgreen),
		       "flooding the area around a box");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testgreen), "the seed pixel was not filled");
	TEST_ASSERT(errctx, pixel_is(shot, TEST_TARGET_SIZE - 1, TEST_TARGET_SIZE - 1, testgreen),
		    "the fill did not reach the far corner of the target");
	TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
		    "the fill from outside overwrote the boundary");

	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_draw_flood_fill(akgl_renderer, -1, 0, testred),
			   "flooding from a seed left of the target");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_draw_flood_fill(akgl_renderer, 0, TEST_TARGET_SIZE, testred),
			   "flooding from a seed below the target");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_flood_fill(NULL, 0, 0, testred),
			   "flooding through a NULL backend");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_copy_and_paste_region(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    SDL_Surface *saved = NULL;
    SDL_Surface *reused = NULL;
    SDL_Rect region;
    SDL_FRect box;

    ATTEMPT {
	// Put something recognisable in the top left corner and save it.
	box.x = 0.0f;
	box.y = 0.0f;
	box.w = 8.0f;
	box.h = 8.0f;
	CATCH(errctx, clear_target());
	CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred));

	region.x = 0;
	region.y = 0;
	region.w = 8;
	region.h = 8;
	TEST_EXPECT_OK(errctx, akgl_draw_copy_region(akgl_renderer, &region, &saved),
		       "saving a region of the target");
	TEST_ASSERT(errctx, saved != NULL, "akgl_draw_copy_region did not allocate a surface");
	TEST_ASSERT(errctx, saved->w == 8 && saved->h == 8,
		    "the saved surface is %dx%d, expected 8x8", saved->w, saved->h);

	// Wipe the screen and put it back somewhere else.
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_paste_region(akgl_renderer, saved, 32.0f, 32.0f),
		       "pasting a saved region");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testred),
		    "the pasted region did not land at its destination");
	TEST_ASSERT(errctx, pixel_is(shot, 39, 39, testred),
		    "the pasted region is smaller than what was saved");
	TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack),
		    "the pasted region is larger than what was saved");
	TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testblack),
		    "pasting also redrew the region at its original position");
	SDL_DestroySurface(shot);
	shot = NULL;

	// A surface the caller already owns is reused rather than replaced, so
	// saving the same region repeatedly does not churn allocations.
	reused = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32);
	FAIL_ZERO_BREAK(errctx, reused, AKGL_ERR_SDL, "%s", SDL_GetError());
	region.x = 32;
	region.y = 32;
	TEST_EXPECT_OK(errctx, akgl_draw_copy_region(akgl_renderer, &region, &reused),
		       "saving into a caller-owned surface");
	TEST_ASSERT(errctx, pixel_is(reused, 0, 0, testred),
		    "the caller-owned surface did not receive the region");

	// Wrong-sized destinations and regions off the edge of the target are
	// refused rather than silently clipped.
	region.w = 4;
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_draw_copy_region(akgl_renderer, &region, &reused),
			   "saving into a destination of the wrong size");
	region.x = TEST_TARGET_SIZE - 4;
	region.y = 0;
	region.w = 8;
	region.h = 8;
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_draw_copy_region(akgl_renderer, &region, &reused),
			   "saving a region that runs off the right edge");
	region.x = 0;
	region.w = 0;
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_draw_copy_region(akgl_renderer, &region, &reused),
			   "saving a region with no area");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_copy_region(NULL, &region, &reused),
			   "saving through a NULL backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_copy_region(akgl_renderer, NULL, &reused),
			   "saving a NULL region");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_copy_region(akgl_renderer, &region, NULL),
			   "saving into a NULL destination");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_paste_region(NULL, saved, 0.0f, 0.0f),
			   "pasting through a NULL backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_paste_region(akgl_renderer, NULL, 0.0f, 0.0f),
			   "pasting a NULL surface");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
	if ( saved != NULL ) {
	    SDL_DestroySurface(saved);
	}
	if ( reused != NULL ) {
	    SDL_DestroySurface(reused);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_preserves_render_draw_color(void)
{
    PREPARE_ERROR(errctx);
    SDL_FRect box;
    uint8_t r = 0;
    uint8_t g = 0;
    uint8_t b = 0;
    uint8_t a = 0;

    ATTEMPT {
	box.x = 0.0f;
	box.y = 0.0f;
	box.w = 4.0f;
	box.h = 4.0f;

	// Drawing must not leave the renderer's color set to whatever it drew
	// with, or the host's next SDL_RenderClear() paints the wrong color.
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x11, 0x22, 0x33, 0x44),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
	CATCH(errctx, akgl_draw_point(akgl_renderer, 1.0f, 1.0f, testred));
	CATCH(errctx, akgl_draw_line(akgl_renderer, 0.0f, 0.0f, 3.0f, 3.0f, testred));
	CATCH(errctx, akgl_draw_rect(akgl_renderer, &box, testred));
	CATCH(errctx, akgl_draw_filled_rect(akgl_renderer, &box, testred));
	CATCH(errctx, akgl_draw_circle(akgl_renderer, 20.0f, 20.0f, 4.0f, testred));

	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_GetRenderDrawColor(akgl_renderer->sdl_renderer, &r, &g, &b, &a),
	    AKGL_ERR_SDL,
	    "%s",
	    SDL_GetError());
	TEST_ASSERT(errctx, (r == 0x11) && (g == 0x22) && (b == 0x33) && (a == 0x44),
		    "drawing left the render draw color at %02x%02x%02x%02x, expected 11223344",
		    r, g, b, a);
    } CLEANUP {
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_backend_without_a_renderer(void)
{
    PREPARE_ERROR(errctx);
    akgl_RenderBackend empty;
    SDL_Surface *saved = NULL;
    SDL_Surface *scratch = NULL;
    SDL_FRect box;
    SDL_Rect region;

    ATTEMPT {
	// A backend that exists but was never given an SDL_Renderer. Every
	// entry point has to say so rather than dereference it -- this is the
	// state a host is in between allocating a backend and initializing it.
	memset(&empty, 0x00, sizeof(akgl_RenderBackend));
	box.x = 0.0f;
	box.y = 0.0f;
	box.w = 4.0f;
	box.h = 4.0f;
	region.x = 0;
	region.y = 0;
	region.w = 4;
	region.h = 4;

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_point(&empty, 0.0f, 0.0f, testred),
			   "plotting through an uninitialized backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_line(&empty, 0.0f, 0.0f, 1.0f, 1.0f, testred),
			   "drawing a line through an uninitialized backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_rect(&empty, &box, testred),
			   "outlining through an uninitialized backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_filled_rect(&empty, &box, testred),
			   "filling through an uninitialized backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_circle(&empty, 4.0f, 4.0f, 2.0f, testred),
			   "drawing a circle through an uninitialized backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_flood_fill(&empty, 0, 0, testred),
			   "flooding through an uninitialized backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_copy_region(&empty, &region, &saved),
			   "saving through an uninitialized backend");
	TEST_ASSERT(errctx, saved == NULL,
		    "a refused save still wrote something to the destination");

	scratch = SDL_CreateSurface(4, 4, SDL_PIXELFORMAT_RGBA32);
	FAIL_ZERO_BREAK(errctx, scratch, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_paste_region(&empty, scratch, 0.0f, 0.0f),
			   "pasting through an uninitialized backend");
    } CLEANUP {
	if ( saved != NULL ) {
	    SDL_DestroySurface(saved);
	}
	if ( scratch != NULL ) {
	    SDL_DestroySurface(scratch);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

/**
 * @brief akgl_draw_background paints its checkerboard and restores the draw colour.
 *
 * Until 0.5.0 this function returned `void`, drew through the global renderer
 * with no check on it, and left the draw colour changed. TODO.md listed it
 * under "needs the offscreen renderer harness" purely because of that global;
 * taking a backend is what makes it testable here alongside everything else in
 * this file.
 */
akerr_ErrorContext *test_draw_background(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    SDL_Color after;
    SDL_Color light = { 0x99, 0x99, 0x99, 0xff };
    SDL_Color dark = { 0x66, 0x66, 0x66, 0xff };

    ATTEMPT {
	CATCH(errctx, clear_target());
	// A deliberately odd colour, so "restored" cannot be confused with
	// "happened to already be black".
	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0x12, 0x34, 0x56, 0x78),
	    AKGL_ERR_SDL, "%s", SDL_GetError());

	TEST_EXPECT_OK(errctx,
		       akgl_draw_background(akgl_renderer, TEST_TARGET_SIZE, TEST_TARGET_SIZE),
		       "painting the transparency checkerboard");

	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());

	// The pattern is 8x8 cells alternating on ((x ^ y) >> 3) & 1, so (0,0)
	// and (8,8) share a colour and (8,0) is the other one.
	TEST_ASSERT(errctx, pixel_is(shot, 0, 0, dark), "cell (0,0) is not the first checker colour");
	TEST_ASSERT(errctx, pixel_is(shot, 7, 7, dark), "cell (0,0) is not filled to its edge");
	TEST_ASSERT(errctx, pixel_is(shot, 8, 0, light), "cell (1,0) is not the second checker colour");
	TEST_ASSERT(errctx, pixel_is(shot, 0, 8, light), "cell (0,1) is not the second checker colour");
	TEST_ASSERT(errctx, pixel_is(shot, 8, 8, dark), "cell (1,1) is not the first checker colour");

	FAIL_ZERO_BREAK(
	    errctx,
	    SDL_GetRenderDrawColor(akgl_renderer->sdl_renderer, &after.r, &after.g, &after.b, &after.a),
	    AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx,
		    (after.r == 0x12) && (after.g == 0x34) && (after.b == 0x56) && (after.a == 0x78),
		    "the draw colour was left at %02x%02x%02x%02x rather than restored",
		    after.r, after.g, after.b, after.a);

	// Degenerate sizes paint nothing and are not an error.
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_background(akgl_renderer, 0, 0),
		       "painting a zero-sized background");
	TEST_EXPECT_OK(errctx, akgl_draw_background(akgl_renderer, -8, -8),
		       "painting a negative-sized background");
	SDL_DestroySurface(shot);
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testblack),
		    "a zero or negative sized background painted something");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_background(NULL, 8, 8),
			   "painting a background through a NULL backend");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_filled_rounded_rect(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    SDL_FRect rect = { 8.0f, 8.0f, 40.0f, 24.0f };
    SDL_FRect square = { 40.0f, 40.0f, 16.0f, 16.0f };

    ATTEMPT {
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &rect, 8.0f, testred),
		       "filling a rounded rectangle");

	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 28, 20, testred),
		    "the middle of the rounded rectangle is unfilled");
	TEST_ASSERT(errctx, pixel_is(shot, 28, 8, testred),
		    "the top edge between the corners is unfilled");
	TEST_ASSERT(errctx, pixel_is(shot, 8, 20, testred),
		    "the left edge between the corners is unfilled");
	// 12,12 is inside the corner's quarter circle (5.7px from its centre
	// at 16,16); 8,8 and 9,9 are outside it (11.3px and 9.9px). The fan's
	// triangles are chords of the circle, so they cover nothing beyond it.
	TEST_ASSERT(errctx, pixel_is(shot, 12, 12, testred),
		    "the inside of the rounded corner is unfilled");
	TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testblack),
		    "the square corner pixel was filled despite the rounding");
	TEST_ASSERT(errctx, pixel_is(shot, 9, 9, testblack),
		    "a pixel outside the corner arc was filled");
	SDL_DestroySurface(shot);
	shot = NULL;

	// A radius beyond half the shorter side clamps to it, which on a
	// square is a circle: centre filled, corners untouched.
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &square, 100.0f, testgreen),
		       "filling a rounded rectangle with an oversized radius");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 48, 48, testgreen),
		    "the centre of the clamped-radius square is unfilled");
	TEST_ASSERT(errctx, pixel_is(shot, 41, 41, testblack),
		    "clamping the radius still filled the square's corner");

	// Radius zero is the plain fill, corners and all.
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &rect, 0.0f, testred),
		       "a zero radius falling through to the plain fill");
	SDL_DestroySurface(shot);
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred),
		    "a zero radius did not fill the square corner");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_filled_rounded_rect(NULL, &rect, 4.0f, testred),
			   "rounded fill through a NULL backend");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_filled_rounded_rect(akgl_renderer, NULL, 4.0f, testred),
			   "rounded fill of a NULL rectangle");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_arc(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;

    ATTEMPT {
	// A full ring: outer radius 20, stroke 4, so mid-stroke is radius 18.
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 360.0f, 4.0f, testred),
		       "stroking a full circle");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 50, 32, testred),
		    "the ring is missing at three o'clock");
	TEST_ASSERT(errctx, pixel_is(shot, 32, 50, testred),
		    "the ring is missing at six o'clock");
	TEST_ASSERT(errctx, pixel_is(shot, 14, 32, testred),
		    "the ring is missing at nine o'clock");
	TEST_ASSERT(errctx, pixel_is(shot, 32, 14, testred),
		    "the ring is missing at twelve o'clock");
	TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack),
		    "the centre of the ring was filled");
	TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testblack),
		    "the stroke bled inside its inner radius");
	SDL_DestroySurface(shot);
	shot = NULL;

	// A quarter arc sweeps clockwise from three o'clock to six o'clock;
	// 44,44 is on its mid-stroke at 45 degrees, twelve o'clock is not in
	// the sweep at all.
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, 4.0f, testgreen),
		       "stroking a quarter arc");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 44, 44, testgreen),
		    "the quarter arc is missing at its midpoint");
	TEST_ASSERT(errctx, pixel_is(shot, 32, 14, testblack),
		    "the quarter arc drew outside its sweep");

	// An empty sweep draws nothing and reports nothing.
	TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 90.0f, 90.0f, 4.0f, testred),
		       "an arc with no sweep");

	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 0.0f, 0.0f, 90.0f, 4.0f, testred),
			   "an arc with no radius");
	TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
			   akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, -1.0f, testred),
			   "an arc with a negative thickness");
	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_arc(NULL, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, 4.0f, testred),
			   "an arc through a NULL backend");
    } CLEANUP {
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
    } PROCESS(errctx) {
    } FINISH(errctx, true);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *test_draw_set_clip(void)
{
    PREPARE_ERROR(errctx);
    SDL_Surface *shot = NULL;
    SDL_Rect clip = { 0, 0, 16, 16 };
    SDL_FRect everything = { 0.0f, 0.0f, (float)TEST_TARGET_SIZE, (float)TEST_TARGET_SIZE };

    ATTEMPT {
	CATCH(errctx, clear_target());
	TEST_EXPECT_OK(errctx, akgl_draw_set_clip(akgl_renderer, &clip),
		       "setting the clip rectangle");
	TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &everything, testred),
		       "filling the whole target under a clip");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred),
		    "the fill did not reach inside the clip rectangle");
	TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack),
		    "the fill escaped the clip rectangle");
	SDL_DestroySurface(shot);
	shot = NULL;

	TEST_EXPECT_OK(errctx, akgl_draw_set_clip(akgl_renderer, NULL),
		       "clearing the clip rectangle");
	TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &everything, testgreen),
		       "filling the whole target after clearing the clip");
	shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
	FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
	TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testgreen),
		    "clearing the clip did not restore the full target");

	TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
			   akgl_draw_set_clip(NULL, &clip),
			   "clipping through a NULL backend");
    } CLEANUP {
	// The clip is renderer state; leaving it set would clip every test
	// after this one.
	IGNORE(akgl_draw_set_clip(akgl_renderer, NULL));
	if ( shot != NULL ) {
	    SDL_DestroySurface(shot);
	}
    } 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());
	akgl_renderer = &akgl_default_renderer;

	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_draw",
		TEST_TARGET_SIZE,
		TEST_TARGET_SIZE,
		0,
		&akgl_window,
		&akgl_renderer->sdl_renderer),
	    AKGL_ERR_SDL,
	    "Couldn't create window/renderer: %s",
	    SDL_GetError());

	CATCH(errctx, test_draw_point());
	CATCH(errctx, test_draw_line());
	CATCH(errctx, test_draw_rects());
	CATCH(errctx, test_draw_circle());
	CATCH(errctx, test_draw_flood_fill());
	CATCH(errctx, test_draw_copy_and_paste_region());
	CATCH(errctx, test_draw_preserves_render_draw_color());
	CATCH(errctx, test_draw_backend_without_a_renderer());
	CATCH(errctx, test_draw_background());
	CATCH(errctx, test_draw_filled_rounded_rect());
	CATCH(errctx, test_draw_arc());
	CATCH(errctx, test_draw_set_clip());
    } CLEANUP {
	SDL_Quit();
    } PROCESS(errctx) {
    } FINISH_NORETURN(errctx);
}
