// testSquirrel.c -- single-threaded Squirrel adapter tests for closure export.
//
// Mirrors testLua's export coverage: a Squirrel closure is pinned as a refcounted
// CalogFnT and invoked from C through the broker, a closure passed as a native
// argument round-trips and is called back, and the export error paths are checked.
// Single-threaded on purpose -- a CalogFnT must be invoked on its owner's VM
// thread, so there is no actor layer here (the threaded engine path is
// testEngineSquirrel). Built under ASan+UBSan.

#include "calogInternal.h"
#include "squirrelAdapter.h"

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

#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)

#define DOUBLER_INPUT    21
#define DOUBLER_EXPECTED 42
#define ADDER_EXPECTED   6

static CalogT          *broker      = NULL;
static CalogSquirrelT *sqctx       = NULL;
static CalogValueT            recorded;
static int32_t           testsRun    = 0;
static int32_t           testsFailed = 0;

static void    checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeApplyTo5(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeRecord(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void    testExportErrors(void);
static void    testExportInvoke(void);
static void    testFunctionValueArg(void);


static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
    testsRun++;
    if (!condition) {
        testsFailed++;
        printf("FAIL %s:%d  %s\n", file, line, message);
    }
}


static int32_t nativeApplyTo5(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    CalogValueT  arg;
    CalogValueT  inner;
    int32_t status;

    (void)userData;
    if (argCount != 1 || args[0].type != calogFnE) {
        return calogFail(result, calogErrArgE, "applyTo5 expects one function");
    }
    // Call the passed-in Squirrel closure back (it was marshalled in as a CalogFnT).
    calogValueInt(&arg, 5);
    status = calogFnInvoke(args[0].as.fn, &arg, 1, &inner);
    calogValueFree(&arg);
    calogValueMove(result, &inner);
    return status;
}


static int32_t nativeRecord(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    int32_t status;

    (void)userData;
    calogValueFree(&recorded);
    calogValueNil(&recorded);
    calogValueNil(result);
    if (argCount != 1) {
        return calogFail(result, calogErrArgE, "record expects one argument");
    }
    status = calogValueCopy(&recorded, &args[0]);
    if (status != calogOkE) {
        return calogFail(result, status, "record failed to copy argument");
    }
    return calogOkE;
}


static void testExportErrors(void) {
    CalogFnT *callable;
    int32_t    status;

    status = calogSquirrelExport(sqctx, "noSuchGlobal", &callable);
    CHECK(status == calogErrNotFoundE && callable == NULL, "exporting an undefined name fails as not-found");

    status = calogSquirrelRun(sqctx, "myNumber <- 5");
    CHECK(status == calogOkE, "define a non-closure global");
    status = calogSquirrelExport(sqctx, "myNumber", &callable);
    CHECK(status == calogErrTypeE && callable == NULL, "exporting a non-closure fails as a type error");
}


static void testExportInvoke(void) {
    CalogFnT *doubler;
    CalogValueT     arg;
    CalogValueT     result;
    int32_t    status;

    status = calogSquirrelRun(sqctx, "function doubler(x) { return x * 2; }");
    CHECK(status == calogOkE, "define doubler");
    status = calogSquirrelExport(sqctx, "doubler", &doubler);
    CHECK(status == calogOkE, "export doubler as a CalogFnT");

    calogValueInt(&arg, DOUBLER_INPUT);
    status = calogFnInvoke(doubler, &arg, 1, &result);
    CHECK(status == calogOkE && result.type == calogIntE && result.as.i == DOUBLER_EXPECTED, "invoke exported Squirrel closure from C");
    calogValueFree(&result);
    calogValueFree(&arg);
    calogFnRelease(doubler);
}


static void testFunctionValueArg(void) {
    int32_t status;

    // The anonymous closure crosses into the native as a CalogFnT, is invoked with
    // 5, and the result (6) is recorded. The CalogFnT is released when the native
    // call's args are freed -- all on this one thread.
    status = calogSquirrelRun(sqctx, "record(applyTo5(function(x) { return x + 1; }))");
    CHECK(status == calogOkE && recorded.type == calogIntE && recorded.as.i == ADDER_EXPECTED, "closure passed as an argument is called back through the broker");
}


int main(void) {
    broker = calogBrokerCreate();
    if (broker == NULL) {
        printf("broker create failed\n");
        return 1;
    }
    if (calogSquirrelCreate(&sqctx, broker, 0) != calogOkE) {
        printf("squirrel context create failed\n");
        return 1;
    }
    calogRegister(broker, "record", nativeRecord, NULL);
    calogRegister(broker, "applyTo5", nativeApplyTo5, NULL);
    calogSquirrelExpose(sqctx, "record");
    calogSquirrelExpose(sqctx, "applyTo5");
    calogValueNil(&recorded);

    testExportInvoke();
    testFunctionValueArg();
    testExportErrors();

    calogValueFree(&recorded);
    calogSquirrelDestroy(sqctx);
    calogBrokerDestroy(broker);

    printf("\n%d checks, %d failed\n", testsRun, testsFailed);
    fflush(stdout);
    if (testsFailed != 0) {
        return 1;
    }
    return 0;
}
