/**
 * @file runtime_verbs.c
 * @brief Tests the verbs the golden corpus never reaches.
 *
 * LIST, DELETE, AUTO, INPUT, DLOAD, DSAVE, EXIT, STOP and the memory verbs are
 * all real code with no `.bas`/`.txt` pair behind them, because the reference's
 * own corpus never exercises them either. They are driven through a whole
 * runtime here rather than unit-tested in pieces: most of them exist to mutate
 * runtime state, and that is the thing worth asserting.
 */

#include <stdio.h>
#include <unistd.h>

#include "harness.h"

/* Run one line as though it had been typed at the REPL. */
static akerr_ErrorContext AKERR_NOIGNORE *run_line(const char *line)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *leaf = NULL;
    akbasic_Value *out = NULL;

    PASS(errctx, akbasic_environment_zero(HARNESS_RUNTIME.environment));
    PASS(errctx, harness_parse(line, &leaf));
    PASS(errctx, akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
    SUCCEED_RETURN(errctx);
}

/* Load a program into the source table the way RUNSTREAM would. */
static void load(const char *lines[], int count)
{
    char scanned[AKBASIC_MAX_LINE_LENGTH];
    int i = 0;

    for ( i = 0; i < count; i++ ) {
	TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
	TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, lines[i], scanned, sizeof(scanned)));
	TEST_REQUIRE_OK(akbasic_runtime_file_line(&HARNESS_RUNTIME, scanned));
    }
}

static void clear_output(void)
{
    memset(HARNESS_OUTPUT, 0, sizeof(HARNESS_OUTPUT));
    rewind(HARNESS_OUT);
}

/**
 * @brief A program read through RUNSTREAM is stored without its line numbers.
 *
 * The load() helper above files what the scanner stripped, which is what every
 * caller inside the library does -- but RUNSTREAM used to file the raw buffer,
 * so a program run from a file held "10 PRINT" under slot 10 and LIST, DSAVE
 * and HELP would each have printed the number twice. This drives the real sink
 * path rather than the helper, because the helper is exactly what was not
 * happening.
 */
static void test_runstream_strips_line_numbers(void)
{
    TEST_REQUIRE_OK(harness_start("10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUNSTREAM));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));

    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"ONE\"");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "PRINT \"TWO\"");

    /* And LIST therefore prints each number once. */
    clear_output();
    TEST_REQUIRE_OK(run_line("LIST"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n");
    harness_stop();
}

int main(void)
{
    static const char *PROGRAM[] = {
	"10 PRINT \"ONE\"",
	"20 PRINT \"TWO\"",
	"30 PRINT \"THREE\"",
    };
    char tmpname[] = "/tmp/akbasic_dsave_XXXXXX";
    char buffer[256];
    akbasic_Value *out = NULL;
    FILE *saved = NULL;
    int fd = -1;

    TEST_REQUIRE_OK(harness_start("42\n"));

    /* PRINT with no argument emits a bare newline. */
    clear_output();
    TEST_REQUIRE_OK(run_line("PRINT"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "\n");

    /*
     * LET does nothing; the assignment already happened during evaluation. A
     * bare LET with no assignment after it is a parse error, in the reference
     * too -- its parse path is just assignment(), which needs an expression.
     */
    TEST_REQUIRE_OK(run_line("LET A# = 5"));
    TEST_REQUIRE_ANY_ERROR(run_line("LET"));

    /* LIST prints the program with its line numbers restored. */
    load(PROGRAM, 3);
    clear_output();
    TEST_REQUIRE_OK(run_line("LIST"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT,
		     "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");

    /* LIST n lists from n to the end. */
    clear_output();
    TEST_REQUIRE_OK(run_line("LIST 20"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");

    /* LIST -n lists from the start through n. */
    clear_output();
    TEST_REQUIRE_OK(run_line("LIST -20"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n");

    /* LIST n-n lists an inclusive range. */
    clear_output();
    TEST_REQUIRE_OK(run_line("LIST 20-30"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");

    /* DSAVE writes the program out; DLOAD reads it back. */
    fd = mkstemp(tmpname);
    TEST_REQUIRE(fd >= 0, "could not create a temporary file");
    if ( fd >= 0 ) {
	close(fd);
	TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
	snprintf(buffer, sizeof(buffer), "DSAVE \"%s\"", tmpname);
	TEST_REQUIRE_OK(run_line(buffer));

	saved = fopen(tmpname, "r");
	TEST_REQUIRE(saved != NULL, "DSAVE produced no file");
	if ( saved != NULL ) {
	    TEST_REQUIRE(fgets(buffer, sizeof(buffer), saved) != NULL, "DSAVE wrote nothing");
	    TEST_REQUIRE_STR(buffer, "10 PRINT \"ONE\"\n");
	    fclose(saved);
	}

	/* DELETE with no range clears the whole program. */
	TEST_REQUIRE_OK(run_line("DELETE"));
	clear_output();
	TEST_REQUIRE_OK(run_line("LIST"));
	TEST_REQUIRE_STR(HARNESS_OUTPUT, "");

	/* DLOAD brings it back. */
	snprintf(buffer, sizeof(buffer), "DLOAD \"%s\"", tmpname);
	TEST_REQUIRE_OK(run_line(buffer));
	clear_output();
	TEST_REQUIRE_OK(run_line("LIST"));
	TEST_REQUIRE_STR(HARNESS_OUTPUT,
			 "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");
	remove(tmpname);
    }

    /* DELETE n-n removes an inclusive range. */
    TEST_REQUIRE_OK(run_line("DELETE 20-30"));
    clear_output();
    TEST_REQUIRE_OK(run_line("LIST"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n");

    /* A missing file is an error, not a crash. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_ANY_ERROR(run_line("DLOAD \"/nonexistent/akbasic/nope.bas\""));

    /* An empty filename is refused before it reaches aksl_fopen. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_STATUS(run_line("DLOAD \"\""), AKBASIC_ERR_VALUE);
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_STATUS(run_line("DLOAD 5"), AKBASIC_ERR_TYPE);

    /* AUTO sets and clears the line-numbering increment. */
    TEST_REQUIRE_OK(run_line("AUTO 10"));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.autoLineNumber, 10);
    TEST_REQUIRE_OK(run_line("AUTO"));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.autoLineNumber, 0);

    /* INPUT prompts and stores. The harness input stream holds "42". */
    clear_output();
    TEST_REQUIRE_OK(run_line("INPUT \"AGE? \" N#"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "AGE? ");
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    {
	akbasic_ASTLeaf *leaf = NULL;
	TEST_REQUIRE_OK(harness_parse("A# = N#", &leaf));
	TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
	TEST_REQUIRE_INT(out->intval, 42);
    }

    /* Mode transitions. */
    TEST_REQUIRE_OK(run_line("STOP"));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_REPL);
    TEST_REQUIRE_OK(run_line("RUN"));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_RUN);
    TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 0);
    TEST_REQUIRE_OK(run_line("RUN 20"));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 20);

    /*
     * QUIT sets a mode. It must not call exit(): a host game decides what
     * quitting means, and this test would not survive it doing otherwise.
     */
    TEST_REQUIRE_OK(run_line("QUIT"));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);
    TEST_REQUIRE_OK(akbasic_runtime_set_mode(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));

    /* State errors: verbs used outside the structure they need. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_STATUS(run_line("RETURN"), AKBASIC_ERR_STATE);
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_STATUS(run_line("EXIT"), AKBASIC_ERR_STATE);
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_STATUS(run_line("NEXT I#"), AKBASIC_ERR_STATE);

    /* GOTO and GOSUB want an integer. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_STATUS(run_line("GOTO \"nope\""), AKBASIC_ERR_TYPE);

    /* GOTO sets the next line. */
    TEST_REQUIRE_OK(run_line("GOTO 30"));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 30);

    /* GOSUB pushes an environment; RETURN pops it and restores the line. */
    {
	akbasic_Environment *before = HARNESS_RUNTIME.environment;
	TEST_REQUIRE_OK(akbasic_environment_zero(before));
	before->lineno = 100;
	TEST_REQUIRE_OK(run_line("GOSUB 500"));
	TEST_REQUIRE(HARNESS_RUNTIME.environment != before, "GOSUB must push an environment");
	TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 500);
	TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->gosubReturnLine, 101);
	TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
	TEST_REQUIRE_OK(run_line("RETURN"));
	TEST_REQUIRE(HARNESS_RUNTIME.environment == before, "RETURN must pop back");
	TEST_REQUIRE_INT(before->nextline, 101);
    }

    /* LABEL records the current line under a name. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    HARNESS_RUNTIME.environment->lineno = 70;
    TEST_REQUIRE_OK(run_line("LABEL AGAIN"));
    {
	int64_t where = 0;
	TEST_REQUIRE_OK(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "AGAIN", &where));
	TEST_REQUIRE_INT(where, 70);
    }

    /* POKE, PEEK and POINTER round-trip through real memory. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(run_line("V# = 255"));
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(run_line("P# = POINTER(V#)"));
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(run_line("Q# = PEEK(P#)"));
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(run_line("POKE P#, 127"));
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(run_line("R# = POINTERVAR(V#)"));

    /* PEEK through a null address is refused rather than segfaulting. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(run_line("Z# = 0"));
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_STATUS(run_line("Y# = PEEK(Z#)"), AKBASIC_ERR_VALUE);

    harness_stop();

    test_runstream_strips_line_numbers();
    return akbasic_test_failures;
}
