/**
 * @file unnumbered.c
 * @brief Programs loaded without line numbers.
 *
 * A script written against `LABEL` and `GOTO NAME` never names a line number, so
 * the numbers it used to be forced to carry were decoration. A loaded line that
 * arrives without one now gets the slot after the last line filed, which is the
 * rule `akbasic_runtime_file_line()` holds for all three loading paths.
 *
 * What is asserted here is the seam, not the language: which slot a line lands
 * in, that both spellings execute identically, that the prompt's rule is
 * untouched, and that a collision is refused rather than losing a line.
 */

#include <stdio.h>
#include <string.h>

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

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

/** @brief Load a program from a string and run it to completion. */
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, harness_start(NULL));
    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 Read a program through the sink, the way the file driver does. */
static akerr_ErrorContext AKERR_NOIGNORE *runstream_program(const char *source)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, harness_start(source));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUNSTREAM));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    SUCCEED_RETURN(errctx);
}

/** @brief A program with no numbers at all runs top to bottom. */
static void test_runs_in_order(void)
{
    TEST_REQUIRE_OK(run_program("PRINT \"ONE\"\n"
				"PRINT \"TWO\"\n"
				"PRINT \"THREE\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nTWO\nTHREE\n");

    /* Slot 0 is left empty and the program starts at 1. */
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[0].code, "");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[1].code, "PRINT \"ONE\"");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[2].code, "PRINT \"TWO\"");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[3].code, "PRINT \"THREE\"");

    /* And every one of them knows it was assigned rather than written. */
    TEST_REQUIRE(!HARNESS_RUNTIME.source[1].numbered, "line 1 should not be marked numbered");
    TEST_REQUIRE(!HARNESS_RUNTIME.source[3].numbered, "line 3 should not be marked numbered");
    harness_stop();
}

/**
 * @brief Two unnumbered lines in a row are two lines.
 *
 * This is the defect the feature replaces, stated on its own: the second used to
 * be filed under the cursor unchanged, on top of the first, with no error and no
 * output. A test that only checked the *last* line would have passed throughout.
 */
static void test_second_line_does_not_replace_the_first(void)
{
    TEST_REQUIRE_OK(run_program("PRINT \"FIRST\"\n"
				"PRINT \"SECOND\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "FIRST\nSECOND\n");
    harness_stop();
}

/** @brief A blank line is whitespace, and costs no slot. */
static void test_blank_lines_cost_nothing(void)
{
    TEST_REQUIRE_OK(run_program("PRINT \"ONE\"\n"
				"\n"
				"\n"
				"PRINT \"TWO\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nTWO\n");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[2].code, "PRINT \"TWO\"");
    harness_stop();
}

/**
 * @brief A numbered line moves the cursor; unnumbered ones follow from there.
 *
 * Execution order is slot order, not file order, which is the whole reason the
 * two can be mixed at all: `50 PRINT "C"` runs before `100 PRINT "A"` however
 * they were written down.
 */
static void test_mixed_numbering(void)
{
    TEST_REQUIRE_OK(run_program("100 PRINT \"A\"\n"
				"PRINT \"B\"\n"
				"50 PRINT \"C\"\n"
				"PRINT \"D\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "C\nD\nA\nB\n");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[50].code, "PRINT \"C\"");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[51].code, "PRINT \"D\"");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[100].code, "PRINT \"A\"");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[101].code, "PRINT \"B\"");

    /* The written numbers are marked as such; the two that followed are not. */
    TEST_REQUIRE(HARNESS_RUNTIME.source[50].numbered, "line 50 was written and should be marked");
    TEST_REQUIRE(HARNESS_RUNTIME.source[100].numbered, "line 100 was written and should be marked");
    TEST_REQUIRE(!HARNESS_RUNTIME.source[51].numbered, "line 51 was assigned, not written");
    TEST_REQUIRE(!HARNESS_RUNTIME.source[101].numbered, "line 101 was assigned, not written");
    harness_stop();
}

/** @brief Branching by label needs no numbers anywhere. */
static void test_label_branching(void)
{
    TEST_REQUIRE_OK(run_program("GOSUB GREET\n"
				"GOTO DONE\n"
				"PRINT \"NOT REACHED\"\n"
				"LABEL GREET\n"
				"PRINT \"HELLO\"\n"
				"RETURN\n"
				"LABEL DONE\n"
				"PRINT \"BYE\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "HELLO\nBYE\n");
    harness_stop();
}

/**
 * @brief A collision involving an assigned number is refused, not overwritten.
 *
 * `1 PRINT "C"` names a slot two unnumbered lines have already been given. Losing
 * one of them to that is not recoverable, so the load fails instead.
 */
static void test_collision_is_refused(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_STATUS(akbasic_runtime_load(&HARNESS_RUNTIME,
					     "PRINT \"A\"\n"
					     "PRINT \"B\"\n"
					     "1 PRINT \"C\"\n"),
			AKBASIC_ERR_BOUNDS);
    harness_stop();

    /* The mirror image: an assigned line landing on one already written. */
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_STATUS(akbasic_runtime_load(&HARNESS_RUNTIME,
					     "2 PRINT \"A\"\n"
					     "1 PRINT \"B\"\n"
					     "PRINT \"C\"\n"),
			AKBASIC_ERR_BOUNDS);
    harness_stop();
}

/** @brief Two lines carrying the same written number still keep the last. */
static void test_duplicate_written_numbers_still_replace(void)
{
    TEST_REQUIRE_OK(run_program("10 PRINT \"OLD\"\n"
				"10 PRINT \"NEW\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "NEW\n");
    harness_stop();
}

/** @brief The same program through the sink lands in the same slots. */
static void test_runstream_assigns_the_same_slots(void)
{
    TEST_REQUIRE_OK(runstream_program("PRINT \"ONE\"\n"
				      "PRINT \"TWO\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nTWO\n");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[1].code, "PRINT \"ONE\"");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[2].code, "PRINT \"TWO\"");
    harness_stop();
}

/**
 * @brief A file ending in a blank line keeps its last line.
 *
 * `tests/language/arithmetic/integer.bas` is this shape, and both this
 * interpreter and the reference used to erase the last line before running it --
 * the blank line was filed under the cursor, which for a line with no number of
 * its own is the number of the line before it.
 */
static void test_trailing_blank_line_keeps_the_last_line(void)
{
    TEST_REQUIRE_OK(runstream_program("PRINT \"ONE\"\n"
				      "PRINT \"LAST\"\n"
				      "\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE\nLAST\n");
    harness_stop();
}

/**
 * @brief The prompt still tells a program line from a statement by its number.
 *
 * The one rule the feature does not touch. `PRINT 2 + 2` typed at a prompt must
 * answer 4 rather than quietly becoming line 1 of a program.
 */
static void test_prompt_still_runs_direct_mode(void)
{
    TEST_REQUIRE_OK(harness_start("PRINT 2 + 2\n10 PRINT \"FILED\"\nLIST\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));

    /* 4 came out now; the numbered line went into the program and LIST found it. */
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "4\n") != NULL,
		 "PRINT 2 + 2 at the prompt should have answered 4, got \"%s\"", HARNESS_OUTPUT);
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "10 PRINT \"FILED\"") != NULL,
		 "the numbered line should have been filed and listed, got \"%s\"", HARNESS_OUTPUT);
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[1].code, "");
    harness_stop();
}

/**
 * @brief Branching by number to a line nobody numbered is refused before the run.
 *
 * `GOTO 2` in a program with no line numbers finds the second line and branches
 * there. The prescan says so instead, and says it before any line executes -- so
 * the program produces the error and nothing else, not the error partway through
 * its output.
 */
static void test_numeric_branch_to_an_assigned_line_is_refused(void)
{
    TEST_REQUIRE_OK(run_program("PRINT \"A\"\n"
				"PRINT \"B\"\n"
				"GOTO 2\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "did not number") != NULL,
		 "expected a refusal naming the unnumbered target, got \"%s\"", HARNESS_OUTPUT);
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "A\n") == NULL,
		 "the program should not have run at all, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    /* The refusal names the offending line, not wherever the loader stopped. */
    TEST_REQUIRE_OK(run_program("GOTO 4\n"
				"PRINT \"B\"\n"
				"PRINT \"C\"\n"
				"PRINT \"D\"\n"
				"PRINT \"E\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 1 :") != NULL,
		 "expected the error to name line 1, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief A written number is still a legal target, in a mixed program too. */
static void test_numeric_branch_to_a_written_line_is_allowed(void)
{
    TEST_REQUIRE_OK(run_program("10 PRINT \"A\"\n"
				"20 IF A# = 0 THEN A# = 1 : GOTO 10\n"
				"30 PRINT \"OK\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "A\nA\nOK\n");
    harness_stop();

    /* Mixed: line 10 was written, so a branch to it stands. */
    TEST_REQUIRE_OK(run_program("10 PRINT \"A\"\n"
				"PRINT \"B\"\n"
				"20 IF A# = 0 THEN A# = 1 : GOTO 10\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "A\nB\nA\nB\n");
    harness_stop();
}

/** @brief A branch to an empty line is left alone, as RENUMBER leaves it alone. */
static void test_numeric_branch_to_an_empty_line_is_allowed(void)
{
    TEST_REQUIRE_OK(run_program("PRINT \"A\"\n"
				"GOTO 500\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "A\n");
    harness_stop();
}

/** @brief A label target never reaches the check, so it cannot be refused by it. */
static void test_label_targets_are_never_refused(void)
{
    TEST_REQUIRE_OK(run_program("GOTO DONE\n"
				"PRINT \"SKIPPED\"\n"
				"LABEL DONE\n"
				"PRINT \"OK\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK\n");
    harness_stop();
}

/** @brief A number inside a string literal is not a branch target. */
static void test_a_number_in_a_string_is_not_a_target(void)
{
    TEST_REQUIRE_OK(run_program("PRINT \"A\"\n"
				"PRINT \"GOTO 2\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "A\nGOTO 2\n");
    harness_stop();
}

/**
 * @brief RENUMBER makes a numberless program branchable by number again.
 *
 * It moves line 3 to line 30 *and* rewrites `GOTO 3` to `GOTO 30`, and every line
 * it touches comes out numbered -- so the branch the check refused a moment ago
 * is now naming a number the program owns. That is the remedy the refusal names.
 */
/**
 * @brief A name that merely *contains* a branch verb is not a branch.
 *
 * The target prescan walks a line character by character looking for `GOTO`,
 * `GOSUB`, `COLLISION` and the rest, and it checked only the character *after* a
 * match. So `RCOLLISION(1, 1)` found `COLLISION` at its second character, read
 * the `(1,` that followed as a handler line number, and refused the whole
 * program with "branch to line 1, which the program did not number" -- naming a
 * line containing no branch at all.
 *
 * `RCOLLISION` is the first name in the language to reach it. The general case
 * is any identifier ending in a branch verb, which is why the second half of
 * this test does not mention collision at all.
 */
static void test_verb_inside_a_name_is_not_a_branch(void)
{
    TEST_REQUIRE_OK(run_program("X# = 0\n"
				"T# = RCOLLISION(1, 1)\n"
				"PRINT \"RAN \" + T#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "RAN 0\n");
    harness_stop();

    /* And the general shape: a variable whose name ends in a branch verb. */
    TEST_REQUIRE_OK(run_program("MYGOTO# = 7\n"
				"PRINT MYGOTO#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "7\n");
    harness_stop();

    /* The trailing check still works: a name *starting* with one is not a branch. */
    TEST_REQUIRE_OK(run_program("GOTOX# = 9\n"
				"PRINT GOTOX#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "9\n");
    harness_stop();
}

static void test_renumber_makes_numeric_branches_legal(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "PRINT \"A\"\n"
					 "GOTO 3\n"
					 "PRINT \"B\"\n"));
    /* Before: line 3 was assigned a number, so branching to it is refused. */
    TEST_REQUIRE_STATUS(akbasic_runtime_check_targets(&HARNESS_RUNTIME), AKBASIC_ERR_SYNTAX);

    TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "GOTO 30");
    TEST_REQUIRE_OK(akbasic_runtime_check_targets(&HARNESS_RUNTIME));
    harness_stop();
}

/** @brief RENUMBER numbers everything, which is what asking for it means. */
static void test_renumber_marks_every_line_numbered(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "PRINT \"ONE\"\n"
					 "PRINT \"TWO\"\n"));
    TEST_REQUIRE(!HARNESS_RUNTIME.source[1].numbered, "line 1 starts out assigned");

    TEST_REQUIRE_OK(akbasic_renumber(&HARNESS_RUNTIME, 10, 10, 0));
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[10].code, "PRINT \"ONE\"");
    TEST_REQUIRE_STR(HARNESS_RUNTIME.source[20].code, "PRINT \"TWO\"");
    TEST_REQUIRE(HARNESS_RUNTIME.source[10].numbered, "RENUMBER should mark line 10 numbered");
    TEST_REQUIRE(HARNESS_RUNTIME.source[20].numbered, "RENUMBER should mark line 20 numbered");
    harness_stop();
}

int main(void)
{
    test_runs_in_order();
    test_second_line_does_not_replace_the_first();
    test_blank_lines_cost_nothing();
    test_mixed_numbering();
    test_label_branching();
    test_collision_is_refused();
    test_duplicate_written_numbers_still_replace();
    test_runstream_assigns_the_same_slots();
    test_trailing_blank_line_keeps_the_last_line();
    test_prompt_still_runs_direct_mode();
    test_numeric_branch_to_an_assigned_line_is_refused();
    test_numeric_branch_to_a_written_line_is_allowed();
    test_numeric_branch_to_an_empty_line_is_allowed();
    test_label_targets_are_never_refused();
    test_a_number_in_a_string_is_not_a_target();
    test_verb_inside_a_name_is_not_a_branch();
    test_renumber_makes_numeric_branches_legal();
    test_renumber_marks_every_line_numbered();
    return akbasic_test_failures;
}
