/**
 * @file interrupts.c
 * @brief Tests the interrupt machinery: arm, raise, enter a handler, RETURN.
 *
 * This is the part of COLLISION that has nothing to do with sprites, and it is
 * tested on its own for two reasons. It is what group C's TRAP will reuse, so it
 * has to be correct before anything is built on it; and driving it through the
 * API rather than through a verb is the only way to fire an interrupt at an
 * exactly known point in a program, which is what most of these assertions turn
 * on.
 *
 * **Every program here is numbered 1, 2, 3, ...** One step of the runtime
 * advances by one source line *index*, empty ones included -- so consecutive
 * numbering is what makes a step count mean a statement count. The first step of
 * any run is line 0, which is always empty, and is accounted for in each count
 * below.
 */

#include <stdio.h>

#include "harness.h"

/**
 * @brief A program with a handler at 3 and a main loop that never ends.
 *
 * The endless loop is on purpose: every test here steps a fixed number of times
 * and asserts what came out, so a regression that fails to enter or fails to
 * return shows up as the wrong output rather than as a run that stops early and
 * looks plausible.
 */
static const char *PROGRAM_BY_LINE =
    "1 PRINT \"START\"\n"
    "2 GOTO 5\n"
    "3 PRINT \"HANDLER\"\n"
    "4 RETURN\n"
    "5 PRINT \"MAIN\"\n"
    "6 GOTO 5\n";

/** @brief Load a program and put the runtime in RUN mode without stepping it. */
static akerr_ErrorContext AKERR_NOIGNORE *load_program(const char *source)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    SUCCEED_RETURN(errctx);
}

/** @brief A handler named by line number runs, and RETURN resumes where it left. */
static akerr_ErrorContext AKERR_NOIGNORE *test_enter_and_return(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, load_program(PROGRAM_BY_LINE));
    PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 3, NULL));

    /* Line 0, then 1 and 2, so the program is sitting on 5 with nothing pending. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\n");

    PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
    TEST_REQUIRE(HARNESS_RUNTIME.handlerenv == NULL, "raising must not enter anything by itself");

    /* One step: the handler is entered and its first line runs. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\nHANDLER\n");
    TEST_REQUIRE(HARNESS_RUNTIME.handlerenv != NULL, "the runtime should know it is in a handler");
    TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
		 "entering the handler should have consumed the event");

    /* The RETURN, then the line the interrupt took the program away from. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\nHANDLER\nMAIN\n");
    TEST_REQUIRE(HARNESS_RUNTIME.handlerenv == NULL, "RETURN should have left the handler");

    /* Still armed: one collision handled is not a subscription cancelled. */
    TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
		 "the handler should still be armed after it returns");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A handler named by label, on a line the program never falls into.
 *
 * This is the shape a handler is actually written in -- jumped over by the main
 * flow, reachable only through the interrupt -- and it works only because
 * akbasic_runtime_scan_labels() files LABEL before anything runs. Without the
 * prescan the label does not exist when the interrupt fires and the run stops
 * with AKBASIC_ERR_UNDEFINED.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_label_handler(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, load_program("1 GOTO 5\n"
			      "2 LABEL BUMPED\n"
			      "3 PRINT \"HANDLER\"\n"
			      "4 RETURN\n"
			      "5 PRINT \"MAIN\"\n"
			      "6 GOTO 5\n"));
    PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
					       0, "BUMPED"));

    /* Line 0, the GOTO, then the main loop's PRINT. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\n");

    PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
    /* The LABEL line the handler is entered on, then the handler's PRINT. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nHANDLER\n");

    /* RETURN, then back into the main loop where it left off. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nHANDLER\nMAIN\n");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief An interrupt does not interrupt an interrupt.
 *
 * Without the guard a collision that is still true while its own handler runs
 * re-enters on the next line, and keeps re-entering until the environment pool
 * is gone -- the failure would be AKBASIC_ERR_ENVIRONMENT from somewhere with
 * nothing to do with sprites. The event raised inside the handler is not lost,
 * though: it is taken as soon as the RETURN lands.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_no_reentry(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, load_program("1 GOTO 5\n"
			      "2 PRINT \"IN\"\n"
			      "3 PRINT \"OUT\"\n"
			      "4 RETURN\n"
			      "5 PRINT \"MAIN\"\n"
			      "6 GOTO 5\n"));
    PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 2, NULL));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\n");

    PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nIN\n");

    /* Fires again while the handler is still running. */
    PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nIN\nOUT\n");
    TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
		 "an event raised inside a handler should be held, not dropped");

    /* The RETURN lands, and the held event is taken on the very next step. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nIN\nOUT\nIN\n");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief A GOSUB the handler itself makes returns without re-arming interrupts. */
static akerr_ErrorContext AKERR_NOIGNORE *test_nested_gosub_keeps_the_flag(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, load_program("1 GOTO 5\n"
			      "2 GOSUB 8\n"
			      "3 RETURN\n"
			      "5 PRINT \"MAIN\"\n"
			      "6 GOTO 5\n"
			      "8 PRINT \"INNER\"\n"
			      "9 RETURN\n"));
    PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 2, NULL));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
    PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));

    /* Enter the handler, which immediately GOSUBs deeper. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    TEST_REQUIRE(HARNESS_RUNTIME.handlerenv != NULL, "the handler should be running");

    /* The inner PRINT and the inner RETURN: still inside the handler. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nINNER\n");
    TEST_REQUIRE(HARNESS_RUNTIME.handlerenv != NULL,
		 "the inner RETURN belongs to the inner GOSUB, not to the handler");

    /* The handler's own RETURN is the one that clears it. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    TEST_REQUIRE(HARNESS_RUNTIME.handlerenv == NULL, "the handler's RETURN should have cleared it");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief Raising an unarmed source records nothing; disarming drops what is held. */
static akerr_ErrorContext AKERR_NOIGNORE *test_arm_and_disarm(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, load_program(PROGRAM_BY_LINE));

    /* Unarmed. A backend may raise every frame without asking who is listening. */
    PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
    TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
		 "an unarmed source should record nothing");

    /* Armed, raised, then taken away again before it could be taken. */
    PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 3, NULL));
    PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
    PASS(errctx, akbasic_runtime_disarm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
    TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
		 "disarming should drop a pending event rather than deferring it");

    /* And the program runs straight through without ever seeing line 3. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 6));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\nMAIN\nMAIN\n");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief NEW disarms everything: the handler lines belong to a program that is gone. */
static akerr_ErrorContext AKERR_NOIGNORE *test_new_disarms(void)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *leaf = NULL;
    akbasic_Value *out = NULL;

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, load_program(PROGRAM_BY_LINE));
    PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 3, NULL));

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

    TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
		 "NEW should have disarmed the interrupt");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A handler label that names nothing is the program's error, not the host's.
 *
 * It has to be *reported* and stop the run. Letting it out of
 * akbasic_runtime_step() would tear down an embedding game over a typo in a
 * script, which is what goal 3 exists to prevent.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_undefined_label_is_reported(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, load_program("1 PRINT \"MAIN\"\n"
			      "2 GOTO 1\n"));
    PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
					       0, "NOSUCHLABEL"));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
    PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));

    /* Returns cleanly -- the error is on the sink, not in the return value. */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "NOSUCHLABEL") != NULL,
		 "the report should name the label that could not be found, got: %s",
		 HARNESS_OUTPUT);
    TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief Arming refuses a source out of range, and a target that is both or neither. */
static akerr_ErrorContext AKERR_NOIGNORE *test_arm_refusals(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));

    TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME,
						      (akbasic_InterruptSource)AKBASIC_MAX_INTERRUPTS,
						      3, NULL),
			AKBASIC_ERR_BOUNDS);
    TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
						      0, NULL),
			AKBASIC_ERR_VALUE);
    TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
						      3, "BUMPED"),
			AKBASIC_ERR_VALUE);
    TEST_REQUIRE_STATUS(akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME,
							(akbasic_InterruptSource)-1),
			AKBASIC_ERR_BOUNDS);
    TEST_REQUIRE_STATUS(akbasic_runtime_disarm_interrupt(&HARNESS_RUNTIME,
							 (akbasic_InterruptSource)AKBASIC_MAX_INTERRUPTS),
			AKBASIC_ERR_BOUNDS);
    TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(NULL, AKBASIC_INTERRUPT_SPRITE, 3, NULL),
			AKERR_NULLPOINTER);
    TEST_REQUIRE_STATUS(akbasic_runtime_service_interrupts(NULL, NULL), AKERR_NULLPOINTER);
    TEST_REQUIRE_STATUS(akbasic_runtime_scan_labels(NULL), AKERR_NULLPOINTER);

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The label prescan reads LABEL where it is a statement, and nowhere else.
 *
 * The forward jump itself is covered by the golden case at
 * tests/language/statements/label_forward.bas. What is here is the part a
 * running program cannot show: that a string literal holding the word LABEL
 * files nothing, and that a second statement on a line is still a statement.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_prescan_boundaries(void)
{
    PREPARE_ERROR(errctx);
    int64_t line = 0;

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, load_program("1 PRINT \"LABEL DECOY\"\n"
			      "2 A# = 1 : LABEL SECOND\n"
			      "3 LABELLED# = 2\n"));

    PASS(errctx, akbasic_environment_get_label(HARNESS_RUNTIME.environment, "SECOND", &line));
    TEST_REQUIRE_INT(line, 2);

    TEST_REQUIRE_STATUS(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "DECOY", &line),
			AKBASIC_ERR_UNDEFINED);
    /* LABELLED# is an identifier that starts with the word, not the verb. */
    TEST_REQUIRE_STATUS(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "ED", &line),
			AKBASIC_ERR_UNDEFINED);

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief A full label table reports the line whose label could not be filed. */
static void test_label_prescan_error_line(void)
{
    char source[4096] = "";
    size_t used = 0;
    int i = 0;

    for ( i = 1; i <= AKBASIC_MAX_LABELS + 1; i++ ) {
	used += (size_t)snprintf(source + used, sizeof(source) - used,
				"%d LABEL L%d\n", i, i);
    }
    (void)snprintf(source + used, sizeof(source) - used, "100 PRINT 1\n");

    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, source));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 65 : PARSE ERROR") != NULL,
		 "a full label table should report its source line, got \"%s\"",
		 HARNESS_OUTPUT);
    harness_stop();
}

int main(void)
{
    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, test_enter_and_return());
	CATCH(errctx, test_label_handler());
	CATCH(errctx, test_no_reentry());
	CATCH(errctx, test_nested_gosub_keeps_the_flag());
	CATCH(errctx, test_arm_and_disarm());
	CATCH(errctx, test_new_disarms());
	CATCH(errctx, test_undefined_label_is_reported());
	CATCH(errctx, test_arm_refusals());
	CATCH(errctx, test_prescan_boundaries());
	test_label_prescan_error_line();
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "interrupt test failed");
	akbasic_test_failures += 1;
    } FINISH_NORETURN(errctx);

    return akbasic_test_failures;
}
