/**
 * @file devices.c
 * @brief Tests the device backend records, the palette table and the host clock.
 *
 * These are the pieces groups G, I and E are built on. Nothing here runs a BASIC
 * verb -- that is what the per-group suites do -- it establishes that a runtime
 * can be given backends, can be given none, and reports the time it was handed.
 */

#include <string.h>

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

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

/**
 * @brief A runtime with no backends attached must still come up and still print.
 *
 * This is the standalone driver's situation and the default for an embedding
 * host that has not opted in, so it is the case that has to keep working.
 */
static void test_no_devices_is_normal(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE(HARNESS_RUNTIME.graphics == NULL, "graphics backend should start NULL");
    TEST_REQUIRE(HARNESS_RUNTIME.audio == NULL, "audio backend should start NULL");
    TEST_REQUIRE(HARNESS_RUNTIME.input == NULL, "input backend should start NULL");
    TEST_REQUIRE_INT(HARNESS_RUNTIME.timems, 0);

    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT \"HI\"\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, "HI\n");
    harness_stop();
}

/** @brief Attaching backends stores them; attaching NULLs takes them away again. */
static void test_set_devices(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();

    TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
						&MOCK_AUDIO, &MOCK_INPUT, NULL));
    TEST_REQUIRE(HARNESS_RUNTIME.graphics == &MOCK_GRAPHICS, "graphics backend was not stored");
    TEST_REQUIRE(HARNESS_RUNTIME.audio == &MOCK_AUDIO, "audio backend was not stored");
    TEST_REQUIRE(HARNESS_RUNTIME.input == &MOCK_INPUT, "input backend was not stored");

    /* Withholding a capability is spelled NULL, and must actually detach. */
    TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL, NULL));
    TEST_REQUIRE(HARNESS_RUNTIME.graphics == &MOCK_GRAPHICS, "graphics backend should have stayed");
    TEST_REQUIRE(HARNESS_RUNTIME.audio == NULL, "audio backend should have been detached");
    TEST_REQUIRE(HARNESS_RUNTIME.input == NULL, "input backend should have been detached");

    TEST_REQUIRE_STATUS(akbasic_runtime_set_devices(NULL, NULL, NULL, NULL, NULL), AKERR_NULLPOINTER);

    /*
     * The fifth device is set on its own, and detaches the same way. It is not a
     * fifth argument above because that signature has twenty-eight call sites
     * and none of them is about the UI.
     */
    TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
    TEST_REQUIRE(HARNESS_RUNTIME.ui == &MOCK_UI, "UI backend was not stored");
    TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, NULL));
    TEST_REQUIRE(HARNESS_RUNTIME.ui == NULL, "UI backend should have been detached");
    TEST_REQUIRE_STATUS(akbasic_runtime_set_ui(NULL, NULL), AKERR_NULLPOINTER);
    harness_stop();
}

/** @brief The clock is stored verbatim, including a value that moves backwards. */
static void test_settime(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));

    TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 1000));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.timems, 1000);

    /*
     * A host driving a paused or scrubbed clock is allowed. Rejecting it would
     * make the interpreter an authority on the host's own time, which it is not.
     */
    TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 400));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.timems, 400);

    TEST_REQUIRE_STATUS(akbasic_runtime_settime(NULL, 0), AKERR_NULLPOINTER);
    harness_stop();
}

/** @brief The palette covers 1 to 16 and refuses everything else. */
static void test_palette(void)
{
    akbasic_Color color;
    int index = 0;

    memset(&color, 0, sizeof(color));

    for ( index = 1; index <= 16; index++ ) {
	TEST_REQUIRE_OK(akbasic_graphics_palette(index, &color));
	TEST_REQUIRE_INT(color.a, 0xff);
    }

    /* Two entries pinned by value, so a shifted table is a failure and not a shrug. */
    TEST_REQUIRE_OK(akbasic_graphics_palette(1, &color));
    TEST_REQUIRE(color.r == 0x00 && color.g == 0x00 && color.b == 0x00,
		 "color 1 should be black, got %02x%02x%02x", color.r, color.g, color.b);
    TEST_REQUIRE_OK(akbasic_graphics_palette(2, &color));
    TEST_REQUIRE(color.r == 0xff && color.g == 0xff && color.b == 0xff,
		 "color 2 should be white, got %02x%02x%02x", color.r, color.g, color.b);

    /*
     * BASIC counts from 1. Zero is the off-by-one somebody writes when they
     * forget that, and it has to be refused rather than quietly return slot 0.
     */
    TEST_REQUIRE_STATUS(akbasic_graphics_palette(0, &color), AKBASIC_ERR_BOUNDS);
    TEST_REQUIRE_STATUS(akbasic_graphics_palette(17, &color), AKBASIC_ERR_BOUNDS);
    TEST_REQUIRE_STATUS(akbasic_graphics_palette(-1, &color), AKBASIC_ERR_BOUNDS);
    TEST_REQUIRE_STATUS(akbasic_graphics_palette(1, NULL), AKERR_NULLPOINTER);
}

/** @brief The mock backends themselves record what they were told. */
static void test_mock_records(void)
{
    akbasic_Color red = { 0xff, 0x00, 0x00, 0xff };
    bool active = false;
    int handle = -1;
    int keycode = 0;
    bool available = false;

    mock_devices_init();

    TEST_REQUIRE_OK(MOCK_GRAPHICS.point(&MOCK_GRAPHICS, 10.0, 20.0, red));
    TEST_REQUIRE_OK(MOCK_GRAPHICS.line(&MOCK_GRAPHICS, 0.0, 0.0, 5.0, 5.0, red));
    TEST_REQUIRE_STR(MOCK.log, "point 10.0,20.0 #ff0000\nline 0.0,0.0-5.0,5.0 #ff0000\n");

    /* A saved shape yields a handle, and only a handle that exists can be pasted. */
    TEST_REQUIRE_OK(MOCK_GRAPHICS.save_shape(&MOCK_GRAPHICS, 0, 0, 8, 8, &handle));
    TEST_REQUIRE_INT(handle, 0);
    TEST_REQUIRE_OK(MOCK_GRAPHICS.paste_shape(&MOCK_GRAPHICS, handle, 16.0, 16.0));
    TEST_REQUIRE_STATUS(MOCK_GRAPHICS.paste_shape(&MOCK_GRAPHICS, 99, 0.0, 0.0), AKBASIC_ERR_BOUNDS);

    /* The exhaustion switch is what lets the PAINT test reach the partial-fill path. */
    MOCK.paint_exhausts = true;
    TEST_REQUIRE_STATUS(MOCK_GRAPHICS.paint(&MOCK_GRAPHICS, 1, 1, red), AKERR_OUTOFBOUNDS);
    MOCK.paint_exhausts = false;
    TEST_REQUIRE_OK(MOCK_GRAPHICS.paint(&MOCK_GRAPHICS, 1, 1, red));

    TEST_REQUIRE_OK(MOCK_AUDIO.tone(&MOCK_AUDIO, 0, 440.0, 250));
    TEST_REQUIRE_OK(MOCK_AUDIO.voice_active(&MOCK_AUDIO, 0, &active));
    TEST_REQUIRE(active, "voice 0 should be sounding after a tone");
    TEST_REQUIRE_OK(MOCK_AUDIO.stop(&MOCK_AUDIO, 0));
    TEST_REQUIRE_OK(MOCK_AUDIO.voice_active(&MOCK_AUDIO, 0, &active));
    TEST_REQUIRE(!active, "voice 0 should be silent after a stop");
    TEST_REQUIRE_STATUS(MOCK_AUDIO.tone(&MOCK_AUDIO, AKBASIC_AUDIO_VOICES, 440.0, 1),
			AKBASIC_ERR_BOUNDS);

    /* An empty key buffer is success with available false, never an error. */
    TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
    TEST_REQUIRE(!available, "an empty mock buffer should report nothing available");

    mock_push_keys("AB");
    TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
    TEST_REQUIRE(available, "a primed mock buffer should report a key");
    TEST_REQUIRE_INT(keycode, 'A');
    TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
    TEST_REQUIRE_INT(keycode, 'B');
    TEST_REQUIRE_OK(MOCK_INPUT.poll_key(&MOCK_INPUT, &keycode, &available));
    TEST_REQUIRE(!available, "the mock buffer should drain to empty");
}

int main(void)
{
    test_no_devices_is_normal();
    test_set_devices();
    test_settime();
    test_palette();
    test_mock_records();
    return akbasic_test_failures;
}
