// testFs.c -- exercises the filesystem library: write/read round-trip (binary-safe), append,
// exists transitions, stat (size + file/dir kind), directory listing, remove, and a catchable
// error, all driven from a Lua context in a private temp directory.

#define _POSIX_C_SOURCE 200809L

#include "calog.h"
#include "calogFs.h"

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

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

#define PUMP_LIMIT   4000
#define RESULT_SLOTS 12

static CalogT         *calog       = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount   = 0;
static _Atomic int32_t errorCount  = 0;
static int32_t         testsRun    = 0;
static int32_t         testsFailed = 0;

static int64_t asInt(const CalogValueT *value);
static void    checkImpl(bool condition, const char *message, const char *file, int32_t line);
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void    onError(uint64_t contextId, const char *message, void *userData);
static void    pumpUntilDone(int32_t target);


static int64_t asInt(const CalogValueT *value) {
    if (value->type == calogIntE) {
        return value->as.i;
    }
    return (int64_t)value->as.r;
}


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 nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    (void)args;
    (void)argCount;
    (void)userData;
    atomic_fetch_add(&doneCount, 1);
    calogValueNil(result);
    return calogOkE;
}


static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    int64_t tag;

    (void)userData;
    calogValueNil(result);
    if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
        return calogFail(result, calogErrArgE, "report expects (tag, value)");
    }
    tag = asInt(&args[0]);
    if (tag < 0 || tag >= RESULT_SLOTS) {
        return calogFail(result, calogErrArgE, "report: tag out of range");
    }
    atomic_store(&results[tag], asInt(&args[1]));
    return calogOkE;
}


static void onError(uint64_t contextId, const char *message, void *userData) {
    (void)contextId;
    (void)userData;
    fprintf(stderr, "  [script error] %s\n", (message != NULL) ? message : "(null)");
    atomic_fetch_add(&errorCount, 1);
}


static void pumpUntilDone(int32_t target) {
    struct timespec ts = { 0, 500000 };
    int             i;

    for (i = 0; i < PUMP_LIMIT; i++) {
        calogPump(calog);
        if (atomic_load(&doneCount) >= target) {
            calogPump(calog);
            return;
        }
        nanosleep(&ts, NULL);
    }
}


int main(void) {
    CalogContextT *ctx;
    char           script[4096];
    char           dir[64];
    int32_t        i;

    calog = calogCreate();
    if (calog == NULL) {
        printf("calog create failed\n");
        return 1;
    }
    calogSetErrorHandler(calog, onError, NULL);
    calogRegister(calog, "report", nativeReport, NULL);
    calogRegister(calog, "done", nativeDone, NULL);
    calogFsRegister(calog);
    for (i = 0; i < RESULT_SLOTS; i++) {
        atomic_store(&results[i], -1);
    }

    // A private per-process temp dir keeps the test hermetic and re-runnable.
    snprintf(dir, sizeof(dir), "/tmp/calogFsTest_%ld", (long)getpid());
    snprintf(script, sizeof(script),
        "local dir = '%s'\n"
        "fsMkdir(dir)\n"
        "local path = dir .. '/data.bin'\n"
        "local data = 'hello\\0world'\n"                                         // 11 bytes, embedded NUL
        "fsWrite(path, data)\n"
        "report(1, fsExists(path) and 1 or 0)\n"                                // 1, the file now exists
        "local back = fsRead(path)\n"
        "report(2, back == data and 1 or 0)\n"                                  // 1, binary-safe round-trip
        "report(3, #back)\n"                                                    // 11, whole file read by size
        "local st = fsStat(path)\n"
        "report(4, st.size)\n"                                                  // 11, stat size
        "report(5, (st.isFile and not st.isDir) and 1 or 0)\n"                  // 1, it is a regular file
        "fsAppend(path, '!')\n"
        "report(6, #fsRead(path))\n"                                            // 12, append added a byte
        "local found = 0\n"
        "for _, name in ipairs(fsList(dir)) do\n"
        "  if name == 'data.bin' then found = 1 end\n"
        "end\n"
        "report(7, found)\n"                                                    // 1, listing sees the file
        "report(8, (fsStat(dir).isDir and not fsStat(dir).isFile) and 1 or 0)\n"  // 1, directory stat
        "local ok = pcall(function() fsRead(dir .. '/nope.dat') end)\n"
        "report(9, ok and 0 or 1)\n"                                            // 1, missing read is catchable
        "fsRemove(path)\n"
        "report(10, fsExists(path) and 0 or 1)\n"                              // 1, exists went false
        "report(11, fsStat(path) == nil and 1 or 0)\n"                         // 1, stat of a missing path is nil
        "fsRemove(dir)\n"
        "done()", dir);

    ctx = calogContextOpen(calog, &calogLuaEngine);
    calogContextEval(ctx, script);
    pumpUntilDone(1);

    CHECK(atomic_load(&results[1]) == 1, "fsWrite then fsExists sees the new file");
    CHECK(atomic_load(&results[2]) == 1, "fsRead round-trips binary data with an embedded NUL");
    CHECK(atomic_load(&results[3]) == 11, "fsRead returns the whole file by byte length");
    CHECK(atomic_load(&results[4]) == 11, "fsStat.size matches the bytes written");
    CHECK(atomic_load(&results[5]) == 1, "fsStat reports a regular file (isFile, not isDir)");
    CHECK(atomic_load(&results[6]) == 12, "fsAppend grew the file by one byte");
    CHECK(atomic_load(&results[7]) == 1, "fsList contains the written file");
    CHECK(atomic_load(&results[8]) == 1, "fsStat reports a directory (isDir, not isFile)");
    CHECK(atomic_load(&results[9]) == 1, "reading a missing file raises a catchable error");
    CHECK(atomic_load(&results[10]) == 1, "fsRemove deletes the file (fsExists transitions to false)");
    CHECK(atomic_load(&results[11]) == 1, "fsStat of a missing path returns nil");
    CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");

    calogContextClose(ctx);
    calogDestroy(calog);

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