/**
 * @file sprite_verbs.c
 * @brief Tests the group H verbs against the recording mock backend.
 *
 * Same shape as tests/graphics_verbs.c and for the same reason: these verbs emit
 * nothing a golden file can compare, so the assertions are on the call log --
 * what reached the device, in what order, with what geometry. Running a real
 * BASIC line rather than calling the handler directly is deliberate; it
 * exercises the dispatch-table row and the parse handler too, which is where an
 * added verb is most likely to be wrong. MOVSPR in particular has a parse
 * handler all its own.
 */

#include <string.h>

#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/sprite.h>

#include "harness.h"
#include "mockdevice.h"
#include "testutil.h"

/** @brief Bring up a runtime with the mock devices attached and a program loaded. */
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, harness_start(NULL));
    mock_devices_init();
    PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
					     &MOCK_AUDIO, &MOCK_INPUT, &MOCK_SPRITES));
    PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief SPRITE sets what it is given and leaves alone what it is not.
 *
 * The "leaves alone" half is the one worth asserting: BASIC 7.0 lets every
 * argument after the number be omitted, so a handler that treated a missing
 * argument as zero would silently turn a sprite off on its way to changing the
 * colour.
 */
static void test_sprite(void)
{
    TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 3, 0, 1, 0\n"));
    /* Palette index 3 is red: 0x88, 0x39, 0x32 in src/graphics_tables.c. */
    TEST_REQUIRE_STR(MOCK.log,
		     "sprcfg 1 136,57,50 x1 y0 b0\n"
		     "sprshow 1 1\n");
    TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].enabled, "SPRITE 1,1 should enable it");
    TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].colorindex, 3);
    harness_stop();

    /* A second SPRITE naming only the number changes nothing at all. */
    TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 3, 0, 1, 0\n"
				"20 SPRITE 1\n"));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].colorindex, 3);
    TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].xexpand,
		 "an omitted argument should leave x-expand alone");
    TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].enabled,
		 "an omitted argument should leave the enable bit alone");
    harness_stop();

    /* Sprite 0 and sprite 9 do not exist. */
    TEST_REQUIRE_OK(run_program("10 SPRITE 9, 1\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..8") != NULL,
		 "sprite 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 17\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..16") != NULL,
		 "colour 17 should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/**
 * @brief All four MOVSPR forms, which is really a test of its parse handler.
 *
 * The separators are the whole difference between them and they exist nowhere
 * else in the language, so this is the only thing that says `;` and `#` are
 * scanned at all.
 */
static void test_movspr(void)
{
    /* Absolute. */
    TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"));
    TEST_REQUIRE_STR(MOCK.log, "sprmove 1 100.00,50.00\n");
    harness_stop();

    /* Relative: a signed coordinate is an offset, not a position. */
    TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"
				"20 MOVSPR 1, +10, -20\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "sprmove 1 100.00,50.00\n"
		     "sprmove 1 110.00,30.00\n");
    harness_stop();

    /*
     * Polar: distance first, then a bearing clockwise from vertical. 90 degrees
     * is due right, so the whole distance lands on x and none of it on y --
     * which is the assertion that catches a sine and cosine swapped over.
     */
    TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"
				"20 MOVSPR 1, 10 ; 90\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "sprmove 1 100.00,50.00\n"
		     "sprmove 1 110.00,50.00\n");
    harness_stop();

    /* And 0 degrees is straight up, which is *minus* y on a screen. */
    TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"
				"20 MOVSPR 1, 10 ; 0\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "sprmove 1 100.00,50.00\n"
		     "sprmove 1 100.00,40.00\n");
    harness_stop();

    /*
     * Continuous. Nothing moves on the statement itself -- the sprite starts
     * moving on the next step, paced off the host's clock -- so what is asserted
     * here is that the speed was recorded and the device was *not* told to move.
     */
    TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 45 # 8\n"));
    TEST_REQUIRE_STR(MOCK.log, "");
    TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].speed, 8);
    TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].angle, 45.0);
    harness_stop();

    /* Speed 16 does not exist. */
    TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 45 # 16\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..15") != NULL,
		 "speed 16 should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/**
 * @brief Continuous motion is paced off the host's clock, not off step count.
 *
 * The service runs on every step, so a program that does nothing still moves --
 * and a host that never sets a clock still gets a still picture rather than a
 * sprite tearing across the screen at step rate.
 */
static void test_continuous_motion(void)
{
    double stopped = 0.0;

    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL,
						&MOCK_SPRITES));
    TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 1000));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "1 MOVSPR 1, 90 # 10\n"
					 "2 GOTO 2\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));

    /* Line 0 and line 1, with the clock standing still. */
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x, 0.0);

    /*
     * One second later: speed 10 at AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND
     * apiece, due right, so 50 pixels of x and none of y.
     */
    TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 2000));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x,
		     10.0 * AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND);
    TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].y, 0.0);

    /*
     * Speed 0 stops it where it is. The step that runs the MOVSPR services the
     * motion *before* it runs the line -- the sprite is still moving when that
     * step begins -- so one more second's worth lands first; where it stopped is
     * whatever it reads after that, and the point is that six more seconds do
     * not move it again.
     */
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "1 MOVSPR 1, 90 # 0\n"));
    HARNESS_RUNTIME.environment->nextline = 1;
    TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 3000));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    stopped = HARNESS_RUNTIME.sprite_state.sprites[0].x;
    TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].speed, 0);

    TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 9000));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x, stopped);
    harness_stop();
}

/** @brief SPRCOLOR sets the two shared registers, and RSPCOLOR reads them back. */
static void test_sprcolor(void)
{
    TEST_REQUIRE_OK(run_program("10 SPRCOLOR 5, 7\n"
				"20 PRINT RSPCOLOR(1)\n"
				"30 PRINT RSPCOLOR(2)\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n7\n");
    harness_stop();

    /* One argument leaves the other register alone. */
    TEST_REQUIRE_OK(run_program("10 SPRCOLOR 5, 7\n"
				"20 SPRCOLOR 9\n"
				"30 PRINT RSPCOLOR(1)\n"
				"40 PRINT RSPCOLOR(2)\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "9\n7\n");
    harness_stop();
}

/**
 * @brief The pattern bit order, asserted once here rather than trusted per backend.
 *
 * Three bytes per row, most significant bit leftmost. Getting it backwards would
 * mirror every published sprite horizontally, and nothing else in the suite can
 * see it: a SPRSAV test fills the array with 0xff, where a reversed bit order
 * produces the identical picture.
 */
static void test_pattern_bit_order(void)
{
    uint8_t pattern[AKBASIC_SPRITE_PATTERN_BYTES];
    uint8_t pixels[AKBASIC_SPRITE_WIDTH * AKBASIC_SPRITE_HEIGHT];
    int i = 0;

    memset(pattern, 0, sizeof(pattern));
    /* Row 0: the leftmost pixel and nothing else. */
    pattern[0] = 0x80;
    /* Row 1: the rightmost pixel of all 24, which is the low bit of the third byte. */
    pattern[5] = 0x01;
    /* Row 2: the ninth pixel across, which is the high bit of the second byte. */
    pattern[7] = 0x80;

    TEST_REQUIRE_OK(akbasic_sprite_unpack(pattern, sizeof(pattern), pixels));
    TEST_REQUIRE_INT(pixels[0], 1);
    TEST_REQUIRE_INT(pixels[1], 0);
    TEST_REQUIRE_INT(pixels[AKBASIC_SPRITE_WIDTH + (AKBASIC_SPRITE_WIDTH - 1)], 1);
    TEST_REQUIRE_INT(pixels[AKBASIC_SPRITE_WIDTH + (AKBASIC_SPRITE_WIDTH - 2)], 0);
    TEST_REQUIRE_INT(pixels[(2 * AKBASIC_SPRITE_WIDTH) + 8], 1);
    TEST_REQUIRE_INT(pixels[(2 * AKBASIC_SPRITE_WIDTH) + 7], 0);

    /* And every other pixel is clear. */
    for ( i = 3 * AKBASIC_SPRITE_WIDTH; i < AKBASIC_SPRITE_WIDTH * AKBASIC_SPRITE_HEIGHT; i++ ) {
	TEST_REQUIRE_INT(pixels[i], 0);
    }

    /* A pattern that is not 63 bytes is not a pattern. */
    TEST_REQUIRE_STATUS(akbasic_sprite_unpack(pattern, 8, pixels), AKBASIC_ERR_BOUNDS);
    TEST_REQUIRE_STATUS(akbasic_sprite_unpack(NULL, sizeof(pattern), pixels), AKERR_NULLPOINTER);
}

/**
 * @brief SPRSAV takes a pattern from a DIMmed integer array.
 *
 * The array form is ours rather than BASIC 7.0's, because a value in this
 * interpreter carries a NUL-terminated string and a C128 puts 63 raw bytes in
 * one -- a string that cannot hold a zero byte cannot hold a sprite. See
 * TODO.md section 5.
 */
static void test_sprsav_from_array(void)
{
    TEST_REQUIRE_OK(run_program("10 DIM P#(63)\n"
				"20 FOR I# = 0 TO 62\n"
				"30 P#(I#) = 255\n"
				"40 NEXT I#\n"
				"50 P#(62) = 129\n"
				"60 SPRSAV P#, 1\n"));
    TEST_REQUIRE(strstr(MOCK.log, "sprdef 1 63 bytes ff..81") != NULL,
		 "the whole pattern should have reached the device, got \"%s\"", MOCK.log);
    TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].defined,
		 "SPRSAV should have marked the sprite defined");
    harness_stop();

    /* An array too small to be a pattern is a program that has miscounted. */
    TEST_REQUIRE_OK(run_program("10 DIM P#(8)\n"
				"20 SPRSAV P#, 1\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "sprite pattern is 63") != NULL,
		 "a short array should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief SPRSAV takes a region SSHAPE saved, told apart by its handle prefix. */
static void test_sprsav_from_shape(void)
{
    TEST_REQUIRE_OK(run_program("10 SSHAPE A$, 0, 0, 24, 21\n"
				"20 SPRSAV A$, 2\n"));
    TEST_REQUIRE(strstr(MOCK.log, "sprshape 2 from 0") != NULL,
		 "the saved region should have reached the sprite device, got \"%s\"",
		 MOCK.log);
    harness_stop();
}

/**
 * @brief SPRSAV takes an image file path, and the program's directory goes with it.
 *
 * Any string that is not an SSHAPE handle is a path. What can be asserted from a
 * mock is the dispatch and the root -- whether a relative path actually resolves
 * is libakgl's, and is asserted against real pixels in tests/akgl_backends.c.
 */
static void test_sprsav_from_file(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL,
						&MOCK_SPRITES));
    TEST_REQUIRE_OK(akbasic_runtime_set_source_path(&HARNESS_RUNTIME, "/games/asteroids/main.bas"));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SPRSAV \"ship.png\", 3\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE_STR(MOCK.log,
		     "sprfile 3 ship.png root=/games/asteroids\n"
		     "sprcfg 3 255,255,255 x0 y0 b0\n");
    harness_stop();

    /* No source path -- a REPL session, or a host that handed over a string. */
    TEST_REQUIRE_OK(run_program("10 SPRSAV \"ship.png\", 3\n"));
    TEST_REQUIRE(strstr(MOCK.log, "root=(none)") != NULL,
		 "with no program file the backend should be told so, got \"%s\"", MOCK.log);
    harness_stop();
}

/** @brief Writing a sprite back out is a disk operation, and is refused by name. */
static void test_sprsav_refuses_to_write(void)
{
    TEST_REQUIRE_OK(run_program("10 SPRSAV 1, \"ship.png\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "disk operation") != NULL,
		 "SPRSAV out to a file should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief COLLISION arms the interrupt, and refuses the two types nothing implements. */
static void test_collision_arming(void)
{
    TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"));
    TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
		 "COLLISION 1 should have armed the sprite interrupt");
    TEST_REQUIRE_INT(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].line, 100);
    harness_stop();

    /* A label is stored by name, not resolved here: the handler may be anywhere. */
    TEST_REQUIRE_OK(run_program("10 COLLISION 1, BUMPED\n"));
    TEST_REQUIRE_STR(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].label, "BUMPED");
    harness_stop();

    /* Omitting the target disarms. */
    TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"
				"20 COLLISION 1\n"));
    TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
		 "COLLISION with no target should disarm");
    harness_stop();

    /*
     * Type 2 arms a *second* handler, on its own interrupt source, and leaves
     * type 1's alone. It used to be refused -- a C128's type 2 is a sprite
     * against the bitmap screen's set pixels, which needs the render target read
     * back every frame -- and SOLID is what turned it into a question this
     * interpreter can answer, against rectangles instead of pixels.
     */
    TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"
				"20 COLLISION 2, 200\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
    TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
		 "COLLISION 1 should still have armed the sprite interrupt");
    TEST_REQUIRE_INT(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].line, 100);
    TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_BACKGROUND].armed,
		 "COLLISION 2 should have armed the background interrupt");
    TEST_REQUIRE_INT(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_BACKGROUND].line, 200);
    harness_stop();

    /* And disarming one leaves the other armed. */
    TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"
				"20 COLLISION 2, 200\n"
				"30 COLLISION 2\n"));
    TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
		 "disarming type 2 should not have disarmed type 1");
    TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_BACKGROUND].armed,
		 "COLLISION 2 with no target should disarm");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 COLLISION 3, 100\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "light pen") != NULL,
		 "COLLISION 3 should be refused with a reason, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 COLLISION 4, 100\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..3") != NULL,
		 "COLLISION 4 should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/**
 * @brief A collision the device reports enters the handler and shows up in BUMP.
 *
 * The whole path: the mock says two sprites overlap, the service ORs that into
 * the accumulator and raises the interrupt, the step loop enters the handler,
 * and BUMP() reports it and clears.
 */
static void test_collision_fires(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL,
						&MOCK_SPRITES));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "1 COLLISION 1, 4\n"
					 "2 PRINT \"MAIN\"\n"
					 "3 GOTO 2\n"
					 "4 PRINT \"HIT\"\n"
					 "5 PRINT BUMP(1)\n"
					 "6 RETURN\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));

    /* Line 0 and the COLLISION, with nothing overlapping yet. */
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "");

    /* Sprites 1 and 3 meet. */
    mock_set_collisions(0x05);
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "HIT") != NULL,
		 "the handler should have been entered, got \"%s\"", HARNESS_OUTPUT);

    /* BUMP reports the mask, and clears it. */
    mock_set_collisions(0);
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "5\n") != NULL,
		 "BUMP should have reported sprites 1 and 3, got \"%s\"", HARNESS_OUTPUT);
    TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.bumped, 0);
    harness_stop();
}

/**
 * @brief SOLID registers static geometry, and COLLISION 2 fires on meeting it.
 *
 * The whole second path, against the mock: the device is told about a rectangle,
 * the scan reports a sprite touching one, the service accumulates it on its own
 * accumulator and raises its own interrupt, and BUMP(2) reports and clears
 * without disturbing BUMP(1).
 */
static void test_solid_geometry(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL,
						&MOCK_SPRITES));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "1 COLLISION 2, 5\n"
					 "2 SOLID 7, 100, 100, 140, 120\n"
					 "3 PRINT \"MAIN\"\n"
					 "4 GOTO 3\n"
					 "5 PRINT \"WALL \" + BUMP(2)\n"
					 "6 PRINT \"SPRITES \" + BUMP(1)\n"
					 "7 RETURN\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));

    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 3));
    TEST_REQUIRE(strstr(MOCK.log, "solid 7,100,100,140,120") != NULL,
		 "SOLID should have reached the device, log was \"%s\"", MOCK.log);

    /* Sprite 1 meets a wall; nothing meets another sprite. */
    mock_set_solid_collisions(0x01);
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "WALL 1") != NULL,
		 "the type 2 handler should have reported sprite 1, got \"%s\"", HARNESS_OUTPUT);

    mock_set_solid_collisions(0);
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    /* The two accumulators are separate: BUMP(1) saw nothing. */
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SPRITES 0") != NULL,
		 "a static collision should not set BUMP(1), got \"%s\"", HARNESS_OUTPUT);
    TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.bumpedsolid, 0);
    harness_stop();

    /* SOLID with no arguments retires every one of them. */
    TEST_REQUIRE_OK(run_program("10 SOLID 3, 0, 0, 10, 10\n"
				"20 SOLID 4, 20, 20, 30, 30\n"
				"30 SOLID\n"));
    TEST_REQUIRE(strstr(MOCK.log, "solid 3 off") != NULL,
		 "bare SOLID should have retired 3, log was \"%s\"", MOCK.log);
    TEST_REQUIRE(strstr(MOCK.log, "solid 4 off") != NULL,
		 "bare SOLID should have retired 4, log was \"%s\"", MOCK.log);
    harness_stop();

    /* Out of range, and a rectangle that is not one. */
    TEST_REQUIRE_OK(run_program("10 SOLID 999, 0, 0, 10, 10\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..") != NULL,
		 "SOLID 999 should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 SOLID 1, 10, 10, 10, 20\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "not a rectangle") != NULL,
		 "a zero-width SOLID should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief The readbacks answer from interpreter state, so they need no device. */
static void test_readbacks(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "10 MOVSPR 1, 120, 90\n"
					 "20 PRINT RSPPOS(1, 0)\n"
					 "30 PRINT RSPPOS(1, 1)\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    /*
     * MOVSPR refuses for want of a device *after* it has moved the sprite, so
     * the program stops on line 10 -- but the position took effect, which is
     * what makes the state readable at all. Read it directly rather than through
     * PRINT, since the program never got that far.
     */
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "MOVSPR needs a sprite device") != NULL,
		 "MOVSPR without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
    TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x, 120.0);
    TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].y, 90.0);
    harness_stop();

    /* With a device attached the same program prints. */
    TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 120, 90\n"
				"20 PRINT RSPPOS(1, 0)\n"
				"30 PRINT RSPPOS(1, 1)\n"
				"40 PRINT RSPPOS(1, 2)\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "120\n90\n0\n");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 6, 1, 1, 0\n"
				"20 PRINT RSPRITE(1, 0)\n"
				"30 PRINT RSPRITE(1, 1)\n"
				"40 PRINT RSPRITE(1, 2)\n"
				"50 PRINT RSPRITE(1, 3)\n"
				"60 PRINT RSPRITE(1, 4)\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n6\n1\n1\n0\n");
    harness_stop();

    /* Out-of-range fields are refused rather than answered with a zero. */
    TEST_REQUIRE_OK(run_program("10 PRINT RSPPOS(1, 9)\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..2") != NULL,
		 "RSPPOS field 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 PRINT RSPCOLOR(3)\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is 1 or 2") != NULL,
		 "RSPCOLOR register 3 should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/**
 * @brief Every verb that needs a device says so by name when it has none.
 *
 * This is the standalone driver's situation, so it is the common path. SPRCOLOR
 * and the readbacks are excluded on purpose: they only touch interpreter state
 * and must keep working before a host has lent the script anything.
 */
static void test_no_device(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SPRSAV \"ship.png\", 1\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SPRSAV needs a sprite device") != NULL,
		 "SPRSAV without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "10 SPRCOLOR 5, 7\n"
					 "20 PRINT RSPCOLOR(1)\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n");
    harness_stop();
}

/** @brief NEW takes the sprites down with the program that defined them. */
static void test_new_clears_sprites(void)
{
    akbasic_ASTLeaf *leaf = NULL;
    akbasic_Value *out = NULL;

    TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 3\n"));
    TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].enabled, "SPRITE should have enabled it");

    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(harness_parse("NEW", &leaf));
    TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));

    TEST_REQUIRE(!HARNESS_RUNTIME.sprite_state.sprites[0].enabled,
		 "NEW should have taken the sprite state down");
    TEST_REQUIRE(strstr(MOCK.log, "sprshow 1 0") != NULL,
		 "NEW should have hidden the sprites on the device, got \"%s\"", MOCK.log);
    harness_stop();
}

int main(void)
{
    test_sprite();
    test_movspr();
    test_continuous_motion();
    test_sprcolor();
    test_pattern_bit_order();
    test_sprsav_from_array();
    test_sprsav_from_shape();
    test_sprsav_from_file();
    test_sprsav_refuses_to_write();
    test_collision_arming();
    test_collision_fires();
    test_solid_geometry();
    test_readbacks();
    test_no_device();
    test_new_clears_sprites();
    return akbasic_test_failures;
}
