// testEngineMyBasic.c -- my-basic on context threads under the actor model. Verifies a
// host native is serviced on the host thread, and that several my-basic contexts run
// concurrently without corruption -- my-basic keeps process-global state, so the engine
// serializes my-basic work with a lock; this is the test that would race without it.
// Built under ASan+UBSan (make test) and ThreadSanitizer (make tsanmb).

#define _POSIX_C_SOURCE 200809L

#include "calog.h"

#include <stdatomic.h>
#include <stdio.h>
#include <time.h>

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

#define PUMP_LIMIT       4000
#define CONTEXT_COUNT    3

static CalogT          *calog     = NULL;
static _Atomic int32_t  bumpCount = 0;
static _Atomic uint64_t bumpCtxId = 0xFFFFu;
static int32_t          testsRun    = 0;
static int32_t          testsFailed = 0;

static int32_t bump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void    checkImpl(bool condition, const char *message, const char *file, int32_t line);
static void    testConcurrentContexts(void);
static void    testHostNative(void);


static int32_t bump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    (void)args;
    (void)argCount;
    (void)userData;
    atomic_store(&bumpCtxId, calogCurrentId());
    atomic_fetch_add(&bumpCount, 1);
    calogValueNil(result);
    return calogOkE;
}


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 void testConcurrentContexts(void) {
    CalogContextT  *ctxs[CONTEXT_COUNT];
    struct timespec ts = { 0, 500000 };
    int32_t         i;

    // The scripts allocate (list creation) as they run so the parallel execution paths
    // churn my-basic's now-atomic allocation counter concurrently -- the case that
    // raced before the vendored patch (see make tsanmb).
    atomic_store(&bumpCount, 0);
    for (i = 0; i < CONTEXT_COUNT; i++) {
        ctxs[i] = calogContextOpen(calog, &calogMyBasicEngine);
        calogContextEval(ctxs[i], "a = list(1, 2, 3)\nb = list(4, 5, 6)\nbump()");
    }
    for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < CONTEXT_COUNT; i++) {
        calogPump(calog);
        nanosleep(&ts, NULL);
    }
    CHECK(atomic_load(&bumpCount) == CONTEXT_COUNT, "several concurrent my-basic contexts all reached the host native");
    for (i = 0; i < CONTEXT_COUNT; i++) {
        calogContextClose(ctxs[i]);
    }
}


static void testHostNative(void) {
    CalogContextT  *ctx;
    struct timespec ts = { 0, 500000 };
    int32_t         i;

    ctx = calogContextOpen(calog, &calogMyBasicEngine);
    CHECK(ctx != NULL, "opened a my-basic context");
    atomic_store(&bumpCount, 0);
    calogContextEval(ctx, "bump()");
    for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 1; i++) {
        calogPump(calog);
        nanosleep(&ts, NULL);
    }
    CHECK(atomic_load(&bumpCount) == 1, "the my-basic script's native call ran");
    CHECK(atomic_load(&bumpCtxId) == 0, "the native ran on the host thread (id 0)");
    calogContextClose(ctx);
}


int main(void) {
    calog = calogCreate();
    if (calog == NULL) {
        printf("calog create failed\n");
        return 1;
    }
    calogRegister(calog, "bump", bump, NULL);

    testHostNative();
    testConcurrentContexts();

    calogDestroy(calog);

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