/**
 * @file input_verbs.c
 * @brief Tests GET, GETKEY and SCNCLR against the recording mock backend.
 *
 * The interesting assertion in here is that GETKEY holds the program without
 * blocking the caller. The test drives the step loop by hand and checks that the
 * program counter does not move while the key buffer is empty, that the step
 * still returns, and that the program resumes the moment a key turns up.
 */

#include <string.h>

#include <akbasic/error.h>
#include <akbasic/input.h>
#include <akbasic/runtime.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, NULL));
    PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief GET on an empty buffer yields the empty string and no error.
 *
 * This is the common case, not the edge case: it is what happens on most
 * iterations of every GET loop ever written.
 */
static void test_get_empty(void)
{
    TEST_REQUIRE_OK(run_program("10 GET A$\n20 PRINT \"[\" + A$ + \"]\"\n"));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "[]\n");
    harness_stop();
}

/** @brief GET takes one key per call, in the order they were typed. */
static void test_get_drains_in_order(void)
{
    TEST_REQUIRE_OK(run_program("10 GET A$\n20 PRINT A$\n30 GET A$\n40 PRINT A$\n"));
    mock_push_keys("HI");
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "H\nI\n");
    harness_stop();
}

/**
 * @brief A numeric variable receives the key code rather than the character.
 *
 * That is what a program testing for a cursor key or a function key wants, and
 * no key is code zero -- which is what a C128 reports too.
 */
static void test_get_numeric(void)
{
    TEST_REQUIRE_OK(run_program("10 GET A#\n20 PRINT A#\n"));
    mock_push_keys("A");
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "65\n");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 GET A#\n20 PRINT A#\n"));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");
    harness_stop();
}

/**
 * @brief GETKEY holds the program without blocking the caller.
 *
 * The assertion that matters. A C128 sits on the keyboard inside GETKEY; here
 * every step still returns, a bounded run() still comes back, and the program
 * simply does not advance -- which is the part a program author cares about.
 */
static void test_getkey_holds_without_blocking(void)
{
    int i = 0;

    /*
     * Lines 1 and 2 rather than the customary 10 and 20, because a step budget
     * has to clear them. Source lines are stored indexed by line number and the
     * cursor starts at zero, so a step spends itself on each empty slot on the
     * way: a program at line 10 needs eleven steps before it has run anything.
     * That is worth knowing before writing any bounded-run test.
     */
    TEST_REQUIRE_OK(run_program("1 GETKEY A$\n2 PRINT \"GOT \" + A$\n"));

    /*
     * A bounded run with an empty buffer returns rather than hanging, and the
     * program has not reached line 2. If GETKEY blocked, this call would never
     * come back and the test would time out instead of failing.
     */
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 20));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
    TEST_REQUIRE(HARNESS_RUNTIME.input_state.waiting,
		 "GETKEY should be holding the program");

    /* Any number of further steps changes nothing while the buffer is empty. */
    for ( i = 0; i < 10; i++ ) {
	TEST_REQUIRE_OK(akbasic_runtime_step(&HARNESS_RUNTIME));
    }
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "");

    /* A keystroke releases it, and the program picks up where it left off. */
    mock_push_keys("X");
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "GOT X\n");
    TEST_REQUIRE(!HARNESS_RUNTIME.input_state.waiting,
		 "GETKEY should have released once a key arrived");
    harness_stop();
}

/** @brief A key already waiting satisfies GETKEY on the spot. */
static void test_getkey_immediate(void)
{
    TEST_REQUIRE_OK(run_program("10 GETKEY A$\n20 PRINT \"GOT \" + A$\n"));
    mock_push_keys("Z");
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "GOT Z\n");
    TEST_REQUIRE(!HARNESS_RUNTIME.input_state.waiting,
		 "GETKEY should not have held when a key was already waiting");
    harness_stop();
}

/**
 * @brief Taking the device away mid-GETKEY releases the hold rather than wedging.
 *
 * A host is allowed to change its mind about what it lends out, and a script
 * stuck forever on a device that no longer exists is the worst of the available
 * outcomes.
 */
static void test_getkey_device_withdrawn(void)
{
    TEST_REQUIRE_OK(run_program("1 GETKEY A$\n2 PRINT \"RELEASED\"\n"));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 20));
    TEST_REQUIRE(HARNESS_RUNTIME.input_state.waiting, "GETKEY should be holding");

    TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL, NULL));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "RELEASED\n");
    harness_stop();
}

/** @brief SCNCLR goes through the sink, so it needs no input device at all. */
static void test_scnclr(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SCNCLR\n20 PRINT \"AFTER\"\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    /* The stdio sink treats a clear as a no-op: clearing a pipe means nothing. */
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER\n");
    harness_stop();
}

/** @brief GET and GETKEY name themselves when there is no device, and check types. */
static void test_errors(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 GET A$\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, "GET needs an input device") != NULL,
		 "GET without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    /* A float variable is neither a character nor a key code. */
    TEST_REQUIRE_OK(run_program("10 GET A%\n"));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "string or integer variable") != NULL,
		 "GET into a float should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

int main(void)
{
    test_get_empty();
    test_get_drains_in_order();
    test_get_numeric();
    test_getkey_holds_without_blocking();
    test_getkey_immediate();
    test_getkey_device_withdrawn();
    test_scnclr();
    test_errors();
    return akbasic_test_failures;
}
