/**
 * @file value_pool.c
 * @brief Tests that creating a name inside a scope costs the value pool nothing.
 *
 * This is the defect a Breakout written against the interpreter died of after
 * twenty-five seconds, and nothing in either corpus could have found it: the
 * pool holds 4096 values, so it takes *thousands* of scope entries to see, and
 * nothing else here runs that long.
 *
 * The cause was that scope exit returns the variable *slot* and not its storage.
 * akbasic_runtime_prev_environment() marks the variable unused,
 * akbasic_runtime_new_variable() memsets the slot it hands back -- clearing
 * `values` -- and akbasic_variable_init() therefore drew fresh slots for a
 * variable whose old ones were still counted. Every GOSUB that created a local
 * leaked, with no diagnostic until the pool ran dry on whichever line happened
 * to be unlucky.
 *
 * A scalar now lives in the variable itself, so the pool is not involved at all.
 * Two of the counts below are past the old 4096 ceiling on purpose -- under it,
 * they would pass against the broken interpreter too. The third takes the
 * opposite approach and asks for the whole pool after only a few scopes, which
 * is cheaper and sharper: one leaked slot and it has nowhere to go.
 */

#include <string.h>

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

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

/** @brief Run a program to completion, bounded so a hang fails rather than waits. */
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, 2000000));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief 6,000 GOSUBs, each creating a local, cost nothing.
 *
 * TODO.md section 6 item 30's reduction, unchanged. It used to fail at
 * `LOC# = 1` on the 4091st call with "Array of 1 elements does not fit in the
 * 0 remaining value slots" -- naming the line that was unlucky rather than the
 * line that was wrong, which is most of why it cost an evening to find.
 */
static void test_scoped_scalar_costs_nothing(void)
{
    TEST_REQUIRE_OK(run_program("10 X# = 0\n"
				"20 FOR T# = 1 TO 6000\n"
				"30   GOSUB SUBA\n"
				"40 NEXT T#\n"
				"50 PRINT \"OK \" + X#\n"
				"60 END\n"
				"70 LABEL SUBA\n"
				"80 LOC# = 1\n"
				"90 X# = X# + LOC#\n"
				"100 RETURN\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK 6000\n");
    harness_stop();
}

/**
 * @brief A `FOR` counter is a name like any other, and costs nothing either.
 *
 * Worth its own case: the counter is created by the loop rather than by an
 * assignment the author wrote, so it was the half of this defect that a program
 * could hit without appearing to create anything at all.
 *
 * `TO 2` rather than `TO 1` because equal bounds run the body zero times in this
 * dialect -- see docs/13-differences.md. A body that never runs would pass this
 * test for the wrong reason.
 */
static void test_scoped_loop_counter_costs_nothing(void)
{
    TEST_REQUIRE_OK(run_program("10 X# = 0\n"
				"20 FOR T# = 1 TO 6000\n"
				"30   GOSUB SUBA\n"
				"40 NEXT T#\n"
				"50 PRINT \"OK \" + X#\n"
				"60 END\n"
				"70 LABEL SUBA\n"
				"80 FOR INNER# = 1 TO 2\n"
				"90   X# = X# + 1\n"
				"100 NEXT INNER#\n"
				"110 RETURN\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK 12000\n");
    harness_stop();
}

/**
 * @brief The pool is genuinely untouched, not merely large enough.
 *
 * Two hundred scope entries and then the pool's *entire* width -- two arrays,
 * because AKBASIC_MAX_ARRAY_ELEMENTS caps any one of them at 1024 while the pool
 * holds 2048. One leaked slot and the second `DIM` has nowhere to go, which
 * makes this the sharpest of the three and by far the cheapest: the two above
 * prove a program finishes, this proves nothing at all was spent. The long ones
 * stay because they are the shape the defect was found in.
 */
static void test_pool_is_untouched_by_scopes(void)
{
    TEST_REQUIRE_OK(run_program("10 FOR T# = 1 TO 200\n"
				"20   GOSUB SUBA\n"
				"30 NEXT T#\n"
				"40 DIM A#(1024)\n"
				"50 DIM B#(1024)\n"
				"70 B#(1023) = 7\n"
				"90 PRINT B#(1023)\n"
				"100 END\n"
				"110 LABEL SUBA\n"
				"120 LOC# = 1\n"
				"130 RETURN\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "7\n");
    harness_stop();
}

/**
 * @brief A structure keeps pool storage, which is what a pointer relies on.
 *
 * The exclusion that makes the fix safe. docs/16-structures.md says nothing is
 * reclaimed and akbasic_runtime_prev_environment() says a pointer into a record
 * DIMmed in a scope stays sound after the scope is gone -- so a `@` name must
 * *not* move into the variable, where the next occupant of the slot would
 * overwrite it. A single-field TYPE is the case that would slip through a test
 * written against size alone.
 */
static void test_structure_storage_stays_in_the_pool(void)
{
    akbasic_Variable *variable = NULL;

    TEST_REQUIRE_OK(run_program("10 TYPE ONE\n"
				"20   N#\n"
				"30 END TYPE\n"
				"40 DIM R@ AS ONE\n"
				"50 R@.N# = 5\n"
				"60 PRINT R@.N#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n");

    TEST_REQUIRE_OK(akbasic_environment_get(HARNESS_RUNTIME.environment, "R@", &variable));
    TEST_REQUIRE(variable != NULL, "the structure variable must exist");
    TEST_REQUIRE(variable->values != &variable->inlinevalue,
		 "a structure must keep pool storage, not inline storage");
    harness_stop();
}

/**
 * @brief A scalar's storage is inside the variable, and an array's is not.
 *
 * Asserted directly rather than only through a program, so the mechanism is
 * pinned and not just its consequence. A future change that made arrays inline
 * would pass every test above and break the pointer guarantee.
 */
static void test_storage_lands_where_it_should(void)
{
    akbasic_Variable *scalar = NULL;
    akbasic_Variable *array = NULL;

    TEST_REQUIRE_OK(run_program("10 S# = 1\n"
				"20 DIM A#(4)\n"));

    TEST_REQUIRE_OK(akbasic_environment_get(HARNESS_RUNTIME.environment, "S#", &scalar));
    TEST_REQUIRE(scalar != NULL, "the scalar must exist");
    TEST_REQUIRE(scalar->values == &scalar->inlinevalue,
		 "a scalar must be stored in the variable");

    TEST_REQUIRE_OK(akbasic_environment_get(HARNESS_RUNTIME.environment, "A#", &array));
    TEST_REQUIRE(array != NULL, "the array must exist");
    TEST_REQUIRE(array->values != &array->inlinevalue,
		 "an array must be stored in the pool");
    harness_stop();
}

/**
 * @brief `SWAP` exchanges two scalars, whose storage moves with the record.
 *
 * A regression rather than a feature: SWAP copies the whole variable record, so
 * the `values` pointer that comes over names the *other* variable's inline slot
 * -- which by then holds this variable's own old value. Left alone each side
 * reads back what it started with and SWAP silently does nothing, which is
 * exactly what the housekeeping golden case caught.
 */
static void test_swap_still_exchanges_scalars(void)
{
    TEST_REQUIRE_OK(run_program("10 A# = 1 : B# = 2\n"
				"20 SWAP A#, B#\n"
				"30 PRINT A# : PRINT B#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "2\n1\n");
    harness_stop();
}

int main(void)
{
    test_scoped_scalar_costs_nothing();
    test_scoped_loop_counter_costs_nothing();
    test_pool_is_untouched_by_scopes();
    test_structure_storage_stays_in_the_pool();
    test_storage_lands_where_it_should();
    test_swap_still_exchanges_scalars();
    return akbasic_test_failures;
}
