/**
 * @file housekeeping_verbs.c
 * @brief Tests the group B verbs: NEW, CLR, CONT, TRON, TROFF, SWAP and HELP.
 *
 * The golden case at tests/language/housekeeping/verbs.bas covers what a running
 * program can see of these. What is here is the rest: the refusals, the two
 * verbs that only mean anything at a REPL prompt (CONT and HELP), and the state
 * a program cannot PRINT.
 */

#include "harness.h"

/** @brief Run a program to completion in RUN mode, from a string. */
static akerr_ErrorContext AKERR_NOIGNORE *run_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));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    SUCCEED_RETURN(errctx);
}

/** @brief Evaluate one immediate line against the current runtime state. */
static akerr_ErrorContext AKERR_NOIGNORE *immediate(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);
}

/** @brief NEW empties the program *and* the variables; CLR keeps the program. */
static akerr_ErrorContext AKERR_NOIGNORE *test_new_and_clr(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT \"KEPT\"\n"));
    PASS(errctx, immediate("A# = 5"));

    PASS(errctx, immediate("CLR"));
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"KEPT\"");
    PASS(errctx, immediate("PRINT A#"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");

    PASS(errctx, immediate("NEW"));
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "");
    TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->lineno, 0);
    TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 0);

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief CLR releases the value pool, so an array can be re-DIMmed indefinitely.
 *
 * The pool is a bump allocator and re-DIMming larger abandons the old slice
 * (see akbasic_ValuePool), so a program that grows an array in a loop exhausts
 * it. CLR is the only thing that hands the storage back, which makes this the
 * assertion that says CLR did something rather than merely appearing to.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_clr_reclaims_storage(void)
{
    PREPARE_ERROR(errctx);
    int i = 0;

    TEST_REQUIRE_OK(harness_start(NULL));

    /* Without CLR this runs out; the loop count is well past the pool's size. */
    for ( i = 0; i < 40; i++ ) {
	PASS(errctx, immediate("DIM Z#(200)"));
	PASS(errctx, immediate("CLR"));
    }
    TEST_REQUIRE_INT(HARNESS_RUNTIME.valuepool.next, 0);

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief CONT refuses when nothing stopped, and resumes when something did. */
static akerr_ErrorContext AKERR_NOIGNORE *test_cont(void)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *leaf = NULL;
    akbasic_Value *out = NULL;

    TEST_REQUIRE_OK(harness_start(NULL));

    /* Nothing has run, so there is nothing to continue. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(harness_parse("CONT", &leaf));
    TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
			AKBASIC_ERR_STATE);

    /* A STOP part-way through, and then a CONT that finishes the program. */
    PASS(errctx, run_program("10 PRINT \"BEFORE\"\n"
			     "20 STOP\n"
			     "30 PRINT \"AFTER\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\nREADY\n");
    TEST_REQUIRE(HARNESS_RUNTIME.stopped, "STOP should have armed CONT");

    PASS(errctx, immediate("CONT"));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    /*
     * The READY is STOP's, not the end of the program's: akbasic_runtime_start()
     * in RUN mode sets run_finished_mode to QUIT, so a program that runs off its
     * own end stops rather than dropping to a prompt.
     */
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "BEFORE\nREADY\nAFTER\n");
    TEST_REQUIRE(!HARNESS_RUNTIME.stopped, "CONT should have disarmed itself");

    /* And a second CONT has nothing left to resume. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(harness_parse("CONT", &leaf));
    TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
			AKBASIC_ERR_STATE);

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief A RUN takes the CONT away: the program is starting over, not resuming. */
static akerr_ErrorContext AKERR_NOIGNORE *test_run_disarms_cont(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, run_program("10 STOP\n"));
    TEST_REQUIRE(HARNESS_RUNTIME.stopped, "STOP should have armed CONT");

    PASS(errctx, immediate("RUN"));
    TEST_REQUIRE(!HARNESS_RUNTIME.stopped, "RUN should have disarmed CONT");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief SWAP refuses what it cannot exchange, and tolerates the identity case. */
static akerr_ErrorContext AKERR_NOIGNORE *test_swap_refusals(void)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *leaf = NULL;
    akbasic_Value *out = NULL;

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, immediate("A# = 1"));
    PASS(errctx, immediate("S$ = \"X\""));

    /* Different types: the suffix is the only type declaration there is. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(harness_parse("SWAP A#, S$", &leaf));
    TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
			AKBASIC_ERR_TYPE);

    /* Not a variable. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(harness_parse("SWAP A#, 5", &leaf));
    TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
			AKBASIC_ERR_SYNTAX);

    /* Only one of them. */
    TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
    TEST_REQUIRE_OK(harness_parse("SWAP A#", &leaf));
    TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out),
			AKBASIC_ERR_SYNTAX);

    /* A variable with itself is legal and leaves it alone, rather than corrupting it. */
    PASS(errctx, immediate("SWAP A#, A#"));
    PASS(errctx, immediate("PRINT A#"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief HELP re-lists the line the last error happened on, and says so when there is none. */
static akerr_ErrorContext AKERR_NOIGNORE *test_help(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, immediate("HELP"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "NO ERROR TO HELP WITH\n");

    /* An out-of-bounds read on line 20, then HELP. */
    TEST_REQUIRE_OK(harness_start(NULL));
    PASS(errctx, run_program("10 DIM Q#(2)\n"
			     "20 PRINT Q#(9)\n"
			     "30 PRINT \"UNREACHED\"\n"));
    TEST_REQUIRE_INT(HARNESS_RUNTIME.errorline, 20);
    PASS(errctx, immediate("HELP"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "20 PRINT Q#(9)") != NULL,
		 "HELP should re-list the line the error happened on, got: %s", HARNESS_OUTPUT);

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/** @brief TRON and TROFF move one flag, and the flag is what the run loop reads. */
static akerr_ErrorContext AKERR_NOIGNORE *test_trace_flag(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE(!HARNESS_RUNTIME.trace, "tracing starts off");
    PASS(errctx, immediate("TRON"));
    TEST_REQUIRE(HARNESS_RUNTIME.trace, "TRON turns tracing on");
    PASS(errctx, immediate("TROFF"));
    TEST_REQUIRE(!HARNESS_RUNTIME.trace, "TROFF turns tracing off");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief An interactive session that errors and then ends does not spin.
 *
 * Not a group B verb, but found while testing them and fixed alongside: the
 * runtime's error class is deliberately sticky, and with run_finished_mode REPL
 * that meant every later step re-entered REPL mode and printed READY again --
 * overwriting the QUIT that end of input had just set. A session that hit one
 * runtime error printed READY forever instead of exiting.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_repl_stops_after_an_error(void)
{
    PREPARE_ERROR(errctx);
    int steps = 0;

    TEST_REQUIRE_OK(harness_start("10 PRINT NOSUCHTHING\nRUN\n"));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));

    /* Bounded, so a regression fails this test rather than hanging the suite. */
    while ( steps < 64 && HARNESS_RUNTIME.mode != AKBASIC_MODE_QUIT ) {
	PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
	steps += 1;
    }
    TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);
    TEST_REQUIRE(steps < 64, "the REPL should quit at end of input, not spin");

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A statement typed with no line number runs; one with a number is stored.
 *
 * Direct mode. The reference only ran the verbs it marked immediate -- RUN, LIST,
 * NEW and the rest -- and filed everything else as program text, so `PRINT 2 + 2`
 * at a prompt silently became line 0 of a program instead of answering. See
 * TODO.md section 5.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_direct_mode(void)
{
    PREPARE_ERROR(errctx);

    TEST_REQUIRE_OK(harness_start("PRINT 2 + 2\n"
				  "A# = 5\n"
				  "PRINT A# * 3\n"
				  "10 PRINT \"STORED\"\n"
				  "RUN\n"));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 200));

    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "4\n") != NULL,
		 "PRINT 2 + 2 should have answered 4, got \"%s\"", HARNESS_OUTPUT);
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "15\n") != NULL,
		 "an assignment then a PRINT should both have run, got \"%s\"",
		 HARNESS_OUTPUT);
    /* And the numbered line was stored rather than run when it was typed. */
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"STORED\"");
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "STORED") != NULL,
		 "RUN should have run the stored line, got \"%s\"", HARNESS_OUTPUT);

    harness_stop();
    SUCCEED_RETURN(errctx);
}

/**
 * @brief A BASIC error typed at the prompt is reported, not raised at the host.
 *
 * Goal 3: a script's mistakes are the script's problem. process_line_run() has
 * always swallowed the context after interpret() put the error line on the sink,
 * but the direct-mode branch of process_line_repl() used a bare PASS and let it
 * out -- so `VERIFY` against a file that did not match, which is an ordinary
 * answer rather than a fault, terminated the driver with a stack trace and took
 * an embedding host with it.
 *
 * VERIFY is used because it is the shortest verb that raises through
 * report_and_reraise() with a message a reader would recognise as their own
 * mistake. Found while making the examples in docs/ execute.
 */
static akerr_ErrorContext AKERR_NOIGNORE *test_direct_mode_error_stays_inside(void)
{
    PREPARE_ERROR(errctx);

    remove("t_direct.bas");
    TEST_REQUIRE_OK(harness_start("10 PRINT \"HI\"\n"
				  "DSAVE \"t_direct.bas\"\n"
				  "20 PRINT \"EXTRA\"\n"
				  "VERIFY \"t_direct.bas\"\n"
				  "PRINT \"STILL HERE\"\n"));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));

    /*
     * PASS, not CATCH: the failure being tested is an error escaping this call,
     * so letting it reach this test's own handler is the assertion.
     */
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 200));

    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "does not match") != NULL,
		 "the mismatch should have been reported to the sink, got \"%s\"",
		 HARNESS_OUTPUT);
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "STILL HERE") != NULL,
		 "the prompt should have carried on after the error, got \"%s\"",
		 HARNESS_OUTPUT);

    harness_stop();
    remove("t_direct.bas");
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, test_new_and_clr());
	CATCH(errctx, test_clr_reclaims_storage());
	CATCH(errctx, test_cont());
	CATCH(errctx, test_run_disarms_cont());
	CATCH(errctx, test_swap_refusals());
	CATCH(errctx, test_help());
	CATCH(errctx, test_trace_flag());
	CATCH(errctx, test_repl_stops_after_an_error());
	CATCH(errctx, test_direct_mode());
	CATCH(errctx, test_direct_mode_error_stays_inside());
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "housekeeping verb test failed");
	akbasic_test_failures += 1;
    } FINISH_NORETURN(errctx);

    return akbasic_test_failures;
}
