/**
 * @file read_data.c
 * @brief Tests READ, DATA and RESTORE against the pre-scanned item list.
 *
 * The reference's READ does not read: it records its identifiers, sets the scope
 * waiting for a DATA verb, and lets execution skip forward until one turns up.
 * Two things follow, and both are asserted here as the defects they were --
 * TODO.md section 6.
 */

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

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

#include "harness.h"
#include "testutil.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, 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 The ordinary case: DATA below its READ, filled in order. */
static void test_read_in_order(void)
{
    TEST_REQUIRE_OK(run_program("10 READ A$, B#\n"
				"20 DATA \"HELLO\", 12345\n"
				"30 PRINT A$\n"
				"40 PRINT B#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "HELLO\n12345\n");
    harness_stop();
}

/**
 * @brief A DATA line *above* its READ is found.
 *
 * It was not: skipping forward from the READ ran off the end of the program in
 * silence, and a second READ re-read the same line.
 */
static void test_data_before_read(void)
{
    TEST_REQUIRE_OK(run_program("10 DATA 1, 2\n"
				"20 READ A#, B#\n"
				"30 PRINT A#\n"
				"40 PRINT B#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\n");
    harness_stop();
}

/**
 * @brief Lines between a READ and its DATA are not skipped.
 *
 * They were, because the skip-forward *was* the implementation. A C128 runs
 * them: READ takes the next item and execution carries on normally.
 */
static void test_no_skipping(void)
{
    TEST_REQUIRE_OK(run_program("10 READ A#\n"
				"20 PRINT \"BETWEEN\"\n"
				"30 DATA 5\n"
				"40 PRINT A#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "BETWEEN\n5\n");
    harness_stop();
}

/** @brief Successive READs walk the cursor along rather than starting over. */
static void test_cursor_advances(void)
{
    TEST_REQUIRE_OK(run_program("10 DATA 1, 2, 3\n"
				"20 READ A#\n"
				"30 READ B#\n"
				"40 READ C#\n"
				"50 PRINT A# + B# + C#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "6\n");
    harness_stop();
}

/** @brief Bare RESTORE goes back to the first item. */
static void test_restore(void)
{
    TEST_REQUIRE_OK(run_program("10 DATA 1, 2, 3\n"
				"20 READ A#\n"
				"30 RESTORE\n"
				"40 READ B#\n"
				"50 PRINT A#\n"
				"60 PRINT B#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n1\n");
    harness_stop();
}

/** @brief RESTORE (line) starts again at the first item on or after that line. */
static void test_restore_line(void)
{
    TEST_REQUIRE_OK(run_program("10 DATA 1, 2\n"
				"20 DATA 30, 40\n"
				"30 RESTORE 20\n"
				"40 READ A#, B#\n"
				"50 PRINT A#\n"
				"60 PRINT B#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n40\n");
    harness_stop();

    /*
     * A line with no DATA on it finds the next one that has some -- a program
     * names the line it wants to read *from*, not one that must hold items.
     */
    TEST_REQUIRE_OK(run_program("10 DATA 1, 2\n"
				"20 REM NOTHING HERE\n"
				"30 DATA 99\n"
				"40 RESTORE 20\n"
				"50 READ A#\n"
				"60 PRINT A#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "99\n");
    harness_stop();
}

/** @brief Reading past the last item is reported rather than yielding zeroes. */
static void test_out_of_data(void)
{
    TEST_REQUIRE_OK(run_program("10 DATA 1\n"
				"20 READ A#, B#\n"
				"30 PRINT \"UNREACHED\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "OUT OF DATA") != NULL,
		 "reading past the end should say so, got \"%s\"", HARNESS_OUTPUT);
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHED") == NULL,
		 "the program should have stopped, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief A quoted item read into a number is a mistake worth reporting. */
static void test_type_mismatch(void)
{
    TEST_REQUIRE_OK(run_program("10 DATA \"TEXT\"\n"
				"20 READ A#\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is a string") != NULL,
		 "a quoted item read into a number should be refused, got \"%s\"",
		 HARNESS_OUTPUT);
    harness_stop();

    /* An unquoted number read into a string is fine: it becomes its text. */
    TEST_REQUIRE_OK(run_program("10 DATA 42\n"
				"20 READ A$\n"
				"30 PRINT A$\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "42\n");
    harness_stop();
}

/** @brief A quoted item may hold commas and colons; that is what quotes are for. */
static void test_quoted_items(void)
{
    TEST_REQUIRE_OK(run_program("10 DATA \"ONE, TWO\", \"A:B\"\n"
				"20 READ A$, B$\n"
				"30 PRINT A$\n"
				"40 PRINT B$\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "ONE, TWO\nA:B\n");
    harness_stop();
}

/** @brief A colon ends a DATA statement, as it does on a Commodore. */
static void test_colon_ends_data(void)
{
    TEST_REQUIRE_OK(run_program("10 DATA 1, 2 : PRINT \"AFTER DATA\"\n"
				"20 READ A#, B#\n"
				"30 PRINT A# + B#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "AFTER DATA\n3\n");
    harness_stop();
}

/** @brief DATA overflow reports the line where the item limit was crossed. */
static void test_data_prescan_error_line(void)
{
    char source[4096] = "";
    size_t used = 0;
    int line = 0;
    int item = 0;

    for ( line = 1; line <= 34; line++ ) {
	used += (size_t)snprintf(source + used, sizeof(source) - used, "%d DATA ", line);
	for ( item = 0; item < 15; item++ ) {
	    used += (size_t)snprintf(source + used, sizeof(source) - used,
				      "%s1", (item == 0 ? "" : ","));
	}
	used += (size_t)snprintf(source + used, sizeof(source) - used, "\n");
    }
	used += (size_t)snprintf(source + used, sizeof(source) - used, "100 DATA 1,1\n");
	(void)snprintf(source + used, sizeof(source) - used, "101 DATA 1\n200 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, "? 101 : PARSE ERROR") != NULL,
		 "DATA overflow should report its source line, got \"%s\"",
		 HARNESS_OUTPUT);
    harness_stop();
}

/** @brief A float item fills a float variable with its fractional part intact. */
static void test_float_items(void)
{
    TEST_REQUIRE_OK(run_program("10 DATA 1.5\n"
				"20 READ A%\n"
				"30 PRINT A%\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "1.500000\n");
    harness_stop();
}

/**
 * @brief A `DATA` line holding a negative number can be read *and* walked past.
 *
 * Two different mechanisms, and only one of them was ever wrong. `READ` scans
 * the source text directly, so it always returned the -5 intact; the statement's
 * own argument parse refused a leading minus, so reaching the line raised
 * "Expected literal". Section 4 settled that `DATA` at run time is a no-op --
 * reaching it means walking past it -- which is what made this reachable at all.
 *
 * TODO.md section 9 item 8. A table of coordinates or velocities is full of
 * negative numbers, which is how a game found it.
 */
static void test_negative_items(void)
{
    TEST_REQUIRE_OK(run_program("10 READ A#\n"
				"20 PRINT \"READ GAVE \" + A#\n"
				"30 DATA -5\n"
				"40 PRINT \"PAST IT\"\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "READ GAVE -5\nPAST IT\n");
    harness_stop();

    /* Mixed signs across a real table, and a negative float. */
    TEST_REQUIRE_OK(run_program("10 DATA -1, 2, -3\n"
				"20 DATA -0.5\n"
				"30 READ A#, B#, C#\n"
				"40 READ D%\n"
				"50 PRINT \"\" + A# + \" \" + B# + \" \" + C# + \" \" + D%\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "-1 2 -3 -0.500000\n");
    harness_stop();
}

int main(void)
{
    test_read_in_order();
    test_data_before_read();
    test_no_skipping();
    test_cursor_advances();
    test_restore();
    test_restore_line();
    test_out_of_data();
    test_type_mismatch();
    test_quoted_items();
    test_colon_ends_data();
	test_data_prescan_error_line();
    test_float_items();
    test_negative_items();
    return akbasic_test_failures;
}
