/**
 * @file hoststruct.c
 * @brief Tests sharing a host C struct with a script.
 *
 * The assertions that matter are the ones about *conversion*, because that is
 * the boundary where the two type systems disagree and where a silent answer
 * would be worst. A range check that quietly wraps is found three frames later
 * in code that did nothing wrong, so every refusal is asserted individually.
 *
 * The sharing itself is asserted in both directions: what the script sees when
 * the host changes its struct underneath, and what the host sees when the script
 * writes.
 */

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>

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

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

typedef struct
{
    char     name[8];
    int8_t   tiny;
    int16_t  small;
    int32_t  hp;
    uint8_t  level;
    float    x;
    double   weight;
    bool     hostile;
} test_Enemy;

static const akbasic_HostField ENEMY_FIELDS[] = {
    /*                  struct       member    BASIC name  C representation           */
    AKBASIC_HOST_FIELD( test_Enemy,  name,     "NAME$",    AKBASIC_HOSTFIELD_CSTRING ),
    AKBASIC_HOST_FIELD( test_Enemy,  tiny,     "TINY#",    AKBASIC_HOSTFIELD_INT8    ),
    AKBASIC_HOST_FIELD( test_Enemy,  small,    "SMALL#",   AKBASIC_HOSTFIELD_INT16   ),
    AKBASIC_HOST_FIELD( test_Enemy,  hp,       "HP#",      AKBASIC_HOSTFIELD_INT32   ),
    AKBASIC_HOST_FIELD( test_Enemy,  level,    "LEVEL#",   AKBASIC_HOSTFIELD_UINT8   ),
    AKBASIC_HOST_FIELD( test_Enemy,  x,        "X%",       AKBASIC_HOSTFIELD_FLOAT   ),
    AKBASIC_HOST_FIELD( test_Enemy,  weight,   "WEIGHT%",  AKBASIC_HOSTFIELD_DOUBLE  ),
    AKBASIC_HOST_FIELD( test_Enemy,  hostile,  "HOSTILE#", AKBASIC_HOSTFIELD_BOOL    )
};

static const akbasic_HostType ENEMY_TYPE = {
    "ENEMY", sizeof(test_Enemy), ENEMY_FIELDS, 8
};

static test_Enemy ENEMY;

/** @brief A fresh runtime with ENEMY registered and bound to FOE@. */
static akerr_ErrorContext AKERR_NOIGNORE *bind_and_run(const char *program)
{
    PREPARE_ERROR(errctx);

    memset(&ENEMY, 0, sizeof(ENEMY));
    snprintf(ENEMY.name, sizeof(ENEMY.name), "GOBLIN");
    ENEMY.tiny = 1;
    ENEMY.small = 2;
    ENEMY.hp = 30;
    ENEMY.level = 3;
    ENEMY.x = 1.5f;
    ENEMY.weight = 2.25;
    ENEMY.hostile = true;

    PASS(errctx, harness_start(NULL));
    PASS(errctx, akbasic_host_register_type(&HARNESS_RUNTIME, &ENEMY_TYPE));
    PASS(errctx, akbasic_host_bind(&HARNESS_RUNTIME, "FOE@", "ENEMY", &ENEMY));
    PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, program));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 4000));
    SUCCEED_RETURN(errctx);
}

/** @brief Every C representation reaches the script as the right BASIC value. */
static void test_reads(void)
{
    TEST_REQUIRE_OK(bind_and_run("10 PRINT FOE@.NAME$\n"
				 "20 PRINT FOE@.TINY#\n"
				 "30 PRINT FOE@.SMALL#\n"
				 "40 PRINT FOE@.HP#\n"
				 "50 PRINT FOE@.LEVEL#\n"
				 "60 PRINT FOE@.WEIGHT%\n"
				 "70 PRINT FOE@.HOSTILE#\n"));
    /* A bool reads as -1, the Commodore convention every condition already uses. */
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "GOBLIN\n1\n2\n30\n3\n2.250000\n-1\n");
    harness_stop();
}

/** @brief A write reaches the host's own struct, with no step in between. */
static void test_writes_reach_the_host(void)
{
    TEST_REQUIRE_OK(bind_and_run("10 FOE@.HP# = FOE@.HP# - 10\n"
				 "20 FOE@.NAME$ = \"ORC\"\n"
				 "30 FOE@.HOSTILE# = 0\n"
				 "40 FOE@.WEIGHT% = 9.5\n"));
    TEST_REQUIRE_INT(ENEMY.hp, 20);
    TEST_REQUIRE_STR(ENEMY.name, "ORC");
    TEST_REQUIRE(ENEMY.hostile == false, "a zero should have cleared the host's bool");
    TEST_REQUIRE_FEQ(ENEMY.weight, 9.5);
    harness_stop();
}

/**
 * @brief The script sees what the host holds *now*, not a snapshot.
 *
 * The half of "shared" that a copy-in binding would get wrong: the game moves
 * its own data between statements and the script has to see it.
 */
static void test_host_changes_are_visible(void)
{
    TEST_REQUIRE_OK(bind_and_run("10 PRINT FOE@.HP#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n");
    harness_stop();

    /* Change it behind the script's back, then run again. */
    TEST_REQUIRE_OK(harness_start(NULL));
    memset(&ENEMY, 0, sizeof(ENEMY));
    ENEMY.hp = 30;
    TEST_REQUIRE_OK(akbasic_host_register_type(&HARNESS_RUNTIME, &ENEMY_TYPE));
    TEST_REQUIRE_OK(akbasic_host_bind(&HARNESS_RUNTIME, "FOE@", "ENEMY", &ENEMY));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT FOE@.HP#\n"));
    ENEMY.hp = 77;
    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, "77\n");
    harness_stop();
}

/**
 * @brief A value that will not fit is refused, by field, rather than wrapped.
 *
 * One case per width, because a range check is exactly the sort of thing that is
 * right for `int32_t` and wrong for `int8_t` when only one of them is tested.
 */
static void test_conversion_refuses_rather_than_truncates(void)
{
    static const char *CASES[] = {
	"10 FOE@.TINY# = 200\n",		/* int8_t  */
	"10 FOE@.SMALL# = 70000\n",		/* int16_t */
	"10 FOE@.LEVEL# = -1\n",		/* uint8_t */
	"10 FOE@.NAME$ = \"MUCH TOO LONG\"\n"	/* char[8] */
    };
    size_t i = 0;

    for ( i = 0; i < sizeof(CASES) / sizeof(CASES[0]); i++ ) {
	TEST_REQUIRE_OK(bind_and_run(CASES[i]));
	TEST_REQUIRE(strstr(HARNESS_OUTPUT, "does not fit") != NULL,
		     "case %zu should be refused rather than truncated, got \"%s\"",
		     i, HARNESS_OUTPUT);
	harness_stop();
    }

    /* And the host's struct is untouched by a refused write. */
    TEST_REQUIRE_OK(bind_and_run("10 FOE@.TINY# = 200\n"));
    TEST_REQUIRE_INT(ENEMY.tiny, 1);
    harness_stop();
}

/**
 * @brief Copy still copies, and POINT still shares -- across the boundary too.
 *
 * The reason there is one API rather than two: the language's own distinction
 * turns out to be exactly the one a host needs, so which one a script used is
 * visible in the listing.
 */
static void test_copy_versus_point(void)
{
    TEST_REQUIRE_OK(bind_and_run("10 TYPE MINE\n"
				 "20   HP#\n"
				 "30 END TYPE\n"
				 "40 DIM SNAP@ AS MINE\n"
				 "50 SNAP@.HP# = FOE@.HP#\n"
				 "60 SNAP@.HP# = 0\n"
				 "70 PRINT FOE@.HP#\n"));
    /* The snapshot moved; the game did not. */
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n");
    TEST_REQUIRE_INT(ENEMY.hp, 30);
    harness_stop();

    TEST_REQUIRE_OK(bind_and_run("10 DIM LIVE@ AS PTR TO ENEMY\n"
				 "20 POINT LIVE@ AT FOE@\n"
				 "30 LIVE@->HP# = 5\n"));
    TEST_REQUIRE_INT(ENEMY.hp, 5);
    harness_stop();
}

/** @brief A suffix that disagrees with the C type is refused at registration. */
static void test_suffix_must_match_the_c_type(void)
{
    static const akbasic_HostField WRONG[] = {
	AKBASIC_HOST_FIELD( test_Enemy, hp, "HP%", AKBASIC_HOSTFIELD_INT32 )
    };
    static const akbasic_HostType WRONG_TYPE = { "WRONGT", sizeof(test_Enemy), WRONG, 1 };
    akerr_ErrorContext *raised = NULL;

    TEST_REQUIRE_OK(harness_start(NULL));
    raised = akbasic_host_register_type(&HARNESS_RUNTIME, &WRONG_TYPE);
    TEST_REQUIRE(raised != NULL, "a float suffix over an int32_t must be refused");
    TEST_REQUIRE_INT(raised->status, AKBASIC_ERR_VALUE);
    test_discard_error(raised);
    harness_stop();
}

/**
 * @brief After unbinding, the name is refused rather than read.
 *
 * The whole reason the entry point exists: a binding is the only pointer this
 * interpreter holds that it did not allocate, so a host has to be able to take
 * it back before the storage goes.
 */
static void test_unbind_refuses_later_reads(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    memset(&ENEMY, 0, sizeof(ENEMY));
    ENEMY.hp = 30;
    TEST_REQUIRE_OK(akbasic_host_register_type(&HARNESS_RUNTIME, &ENEMY_TYPE));
    TEST_REQUIRE_OK(akbasic_host_bind(&HARNESS_RUNTIME, "FOE@", "ENEMY", &ENEMY));
    TEST_REQUIRE_OK(akbasic_host_unbind(&HARNESS_RUNTIME, "FOE@"));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT FOE@.HP#\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, "ERROR") != NULL,
		 "reading an unbound name should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief A binding survives the prescan, which runs again on every RUN. */
static void test_registration_survives_a_rerun(void)
{
    TEST_REQUIRE_OK(bind_and_run("10 PRINT FOE@.HP#\n"));
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n");
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 4000));
    /*
     * The prescan clears what the *script* declared and keeps what the *host*
     * registered. Wiping the table outright unregistered the host's types the
     * first time a script ran, with no way for the host to know it had to
     * register them again.
     */
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n30\n");
    harness_stop();
}

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

    test_reads();
    test_writes_reach_the_host();
    test_host_changes_are_visible();
    test_conversion_refuses_rather_than_truncates();
    test_copy_versus_point();
    test_suffix_must_match_the_c_type();
    test_unbind_refuses_later_reads();
    test_registration_survives_a_rerun();

    return akbasic_test_failures;
}
