/**
 * @file struct_pointers.c
 * @brief Tests strict pointers: `PTR TO`, `POINT ... AT`, and `->`.
 *
 * The assertions worth having here are the *refusals*. A pointer that works is
 * visible in tests/language/structures/; a pointer that should not have worked
 * is not, because the failure mode of a missing check is a program that keeps
 * going and gives the wrong answer later.
 *
 * "Strict" means the two operators do not stand in for one another: `.` needs a
 * structure and `->` needs a pointer, so a reader always knows from the spelling
 * whether the thing on the left is their own copy or somebody else's data.
 */

#include <string.h>

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

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

/** @brief A RECT, an instance, and a pointer already aimed at it. */
static const char *PRELUDE =
    "10 TYPE RECT\n"
    "20   W#\n"
    "30   H#\n"
    "40 END TYPE\n"
    "50 DIM A@ AS RECT\n"
    "60 DIM P@ AS PTR TO RECT\n"
    "70 A@.W# = 3\n"
    "80 A@.H# = 4\n"
    "90 POINT P@ AT A@\n";

/** @brief Run the prelude plus @p tail, and leave the output in HARNESS_OUTPUT. */
static akerr_ErrorContext AKERR_NOIGNORE *run_with(const char *tail)
{
    PREPARE_ERROR(errctx);
    char source[2048];

    snprintf(source, sizeof(source), "%s%s", PRELUDE, tail);
    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, 4000));
    SUCCEED_RETURN(errctx);
}

/** @brief Writing through a pointer changes the thing it points at. */
static void test_pointer_writes_through(void)
{
    TEST_REQUIRE_OK(run_with("100 P@->W# = 99\n110 PRINT A@.W#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "99\n");
    harness_stop();
}

/**
 * @brief Assignment still copies, even with a pointer in the room.
 *
 * The property the whole design rests on: `POINT` is the only way to share, so
 * a program that never writes it can never be surprised by aliasing.
 */
static void test_assignment_still_copies(void)
{
    TEST_REQUIRE_OK(run_with("100 DIM B@ AS RECT\n"
			     "110 B@ = A@\n"
			     "120 P@->W# = 7\n"
			     "130 PRINT A@.W#\n"
			     "140 PRINT B@.W#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "7\n3\n");
    harness_stop();
}

/**
 * @brief The two operators do not stand in for one another.
 *
 * Both messages name the other operator, because the mistake is always one of
 * the two and saying which is most of the fix.
 */
static void test_wrong_operator_refused(void)
{
    TEST_REQUIRE_OK(run_with("100 PRINT P@.W#\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "use -> to reach a field through a pointer") != NULL,
		 "'.' on a pointer should say to use ->, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_with("100 PRINT A@->W#\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "use . to reach a field of a structure") != NULL,
		 "'->' on a structure should say to use ., got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief A pointer that has never been POINTed is not a null dereference. */
static void test_unbound_pointer_refused(void)
{
    TEST_REQUIRE_OK(run_with("100 DIM Q@ AS PTR TO RECT\n110 PRINT Q@->W#\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "not pointing at anything") != NULL,
		 "an unbound pointer should refuse by name, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    /* And it renders as NOTHING rather than as a crash or an empty line. */
    TEST_REQUIRE_OK(run_with("100 DIM Q@ AS PTR TO RECT\n110 PRINT Q@\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "NOTHING\n");
    harness_stop();
}

/** @brief A pointer is to one type and will not be aimed at another. */
static void test_pointer_type_is_checked(void)
{
    TEST_REQUIRE_OK(run_with("100 TYPE OTHER\n"
			     "110   N#\n"
			     "120 END TYPE\n"
			     "130 DIM O@ AS OTHER\n"
			     "140 POINT P@ AT O@\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "cannot be pointed at") != NULL,
		 "a pointer to RECT should refuse an OTHER, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/**
 * @brief A structure cannot be assigned to a pointer, and the message says why.
 *
 * The two are spelled differently on purpose. Allowing `P@ = A@` to mean "point
 * at" would make assignment sometimes copy and sometimes alias, which is the one
 * ambiguity this design exists to avoid.
 */
static void test_structure_not_assignable_to_pointer(void)
{
    TEST_REQUIRE_OK(run_with("100 P@ = A@\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "POINT it AT") != NULL,
		 "assigning a structure to a pointer should say to POINT it, got \"%s\"",
		 HARNESS_OUTPUT);
    harness_stop();
}

/**
 * @brief A list is built, walked and rendered -- the reason pointers exist.
 *
 * Walked with reassignment rather than recursion, deliberately: a recursive
 * multi-line DEF does not return in this interpreter, which is recorded in
 * TODO.md and is not this feature's to fix.
 */
static void test_linked_list(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "10 TYPE NODE\n"
					 "20   COUNT#\n"
					 "30   TAIL@ AS PTR TO NODE\n"
					 "40 END TYPE\n"
					 "50 DIM N1@ AS NODE\n"
					 "60 DIM N2@ AS NODE\n"
					 "70 N1@.COUNT# = 10\n"
					 "80 N2@.COUNT# = 20\n"
					 "90 POINT N1@.TAIL@ AT N2@\n"
					 "100 DIM WALK@ AS PTR TO NODE\n"
					 "110 POINT WALK@ AT N1@\n"
					 "120 PRINT WALK@->COUNT#\n"
					 "130 WALK@ = WALK@->TAIL@\n"
					 "140 PRINT WALK@->COUNT#\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 4000));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "10\n20\n");
    harness_stop();
}

/**
 * @brief A cycle renders to a bounded depth instead of forever.
 *
 * Copy cannot follow a pointer, so no *copy* can loop -- but rendering must
 * follow one or a list would not be worth printing, and two nodes pointing at
 * each other is easy to write by accident.
 */
static void test_cycle_renders_bounded(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "10 TYPE NODE\n"
					 "20   COUNT#\n"
					 "30   TAIL@ AS PTR TO NODE\n"
					 "40 END TYPE\n"
					 "50 DIM A@ AS NODE\n"
					 "60 DIM B@ AS NODE\n"
					 "70 POINT A@.TAIL@ AT B@\n"
					 "80 POINT B@.TAIL@ AT A@\n"
					 "90 PRINT A@\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 4000));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "(...)") != NULL,
		 "a cycle should stop at the depth bound, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

int main(void)
{
    TEST_REQUIRE_OK(akbasic_error_register());

    test_pointer_writes_through();
    test_assignment_still_copies();
    test_wrong_operator_refused();
    test_unbound_pointer_refused();
    test_pointer_type_is_checked();
    test_structure_not_assignable_to_pointer();
    test_linked_list();
    test_cycle_renders_bounded();

    return akbasic_test_failures;
}
