// testDbPg.c -- live Postgres backend test. Given a libpq conninfo string on argv[1],
// drives a create/insert/update/query round-trip from a Lua script through the DB library
// and checks the typed values (int, text, double, bool) that come back.

#define _POSIX_C_SOURCE 200809L

#include "calog.h"
#include "calogDb.h"

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

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

#define PUMP_LIMIT 4000

static CalogT          *calog         = NULL;
static _Atomic int64_t  reportedCount  = -1;
static _Atomic int64_t  reportedId     = -1;
static _Atomic int64_t  execAffected   = -1;
static char             reportedName[32] = { 0 };
static char             reportedBlob[16] = { 0 };
static _Atomic int32_t  reportedBlobLen = -1;
static double           reportedScore  = 0.0;
static _Atomic bool     reportedOk     = true;
static _Atomic bool     scriptDone     = false;
static _Atomic int32_t  errorCount     = 0;
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 nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportBlob(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void    onError(uint64_t contextId, const char *message, void *userData);
static void    pumpUntilDone(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 nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    (void)args;
    (void)argCount;
    (void)userData;
    atomic_store(&scriptDone, true);
    calogValueNil(result);
    return calogOkE;
}


static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    size_t length;

    (void)userData;
    calogValueNil(result);
    if (argCount != 6 || args[0].type != calogIntE || args[1].type != calogIntE || args[2].type != calogStringE || args[3].type != calogIntE || args[4].type != calogRealE || args[5].type != calogBoolE) {
        return calogFail(result, calogErrArgE, "report expects (count, id, name, affected, score, ok)");
    }
    atomic_store(&reportedCount, args[0].as.i);
    atomic_store(&reportedId, args[1].as.i);
    length = (size_t)args[2].as.s.length;
    if (length >= sizeof(reportedName)) {
        length = sizeof(reportedName) - 1;
    }
    memcpy(reportedName, args[2].as.s.bytes, length);
    reportedName[length] = '\0';
    atomic_store(&execAffected, args[3].as.i);
    reportedScore = args[4].as.r;
    atomic_store(&reportedOk, args[5].as.b);
    return calogOkE;
}


static int32_t nativeReportBlob(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    size_t length;

    (void)userData;
    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, "reportBlob expects one string");
    }
    length = (size_t)args[0].as.s.length;
    if (length > sizeof(reportedBlob)) {
        length = sizeof(reportedBlob);
    }
    memcpy(reportedBlob, args[0].as.s.bytes, length);
    atomic_store(&reportedBlobLen, (int32_t)length);
    return calogOkE;
}


static void onError(uint64_t contextId, const char *message, void *userData) {
    (void)contextId;
    (void)userData;
    (void)message;
    atomic_fetch_add(&errorCount, 1);
}


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

    errorsBefore = atomic_load(&errorCount);
    for (i = 0; i < PUMP_LIMIT; i++) {
        calogPump(calog);
        if (atomic_load(&scriptDone) || atomic_load(&errorCount) != errorsBefore) {
            calogPump(calog);
            return;
        }
        nanosleep(&ts, NULL);
    }
}


int main(int argc, char **argv) {
    CalogContextT *ctx;
    char           script[2048];
    const char    *conninfo;

    if (argc < 2) {
        printf("usage: testDbPg <conninfo>\n");
        return 2;
    }
    conninfo = argv[1];

    calog = calogCreate();
    if (calog == NULL) {
        printf("calog create failed\n");
        return 1;
    }
    calogSetErrorHandler(calog, onError, NULL);
    calogRegister(calog, "report", nativeReport, NULL);
    calogRegister(calog, "reportBlob", nativeReportBlob, NULL);
    calogRegister(calog, "done", nativeDone, NULL);
    if (calogDbRegister(calog) != calogOkE) {
        printf("calogDbRegister failed\n");
        return 1;
    }

    snprintf(script, sizeof(script),
        "local db = dbOpen('postgres', '%s')\n"
        "dbExec(db, 'drop table if exists ct')\n"
        "dbExec(db, 'create table ct (id int, name text, score double precision, ok boolean)')\n"
        "dbExec(db, 'insert into ct values ($1, $2, $3, $4)', 1, 'alice', 3.5, true)\n"
        "dbExec(db, 'insert into ct values ($1, $2, $3, $4)', 2, 'bob', 7.25, false)\n"
        "local n = dbExec(db, 'update ct set name = $1 where id = $2', 'bobby', 2)\n"
        "local rows = dbQuery(db, 'select id, name, score, ok from ct order by id')\n"
        "report(#rows, rows[2].id, rows[2].name, n, rows[2].score, rows[2].ok)\n"
        "dbExec(db, 'drop table if exists bb')\n"
        "dbExec(db, 'create table bb (v bytea)')\n"
        "dbExec(db, 'insert into bb values ($1)', 'x\\0y')\n"
        "local br = dbQuery(db, 'select v from bb')\n"
        "reportBlob(br[1].v)\n"
        "dbClose(db)\n"
        "done()",
        conninfo);

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

    CHECK(atomic_load(&reportedCount) == 2, "Postgres dbQuery returned both rows");
    CHECK(atomic_load(&reportedId) == 2, "the integer column read back typed");
    CHECK(strcmp(reportedName, "bobby") == 0, "a bound-param update took effect; text read back");
    CHECK(atomic_load(&execAffected) == 1, "dbExec reported the correct rows-affected");
    CHECK(reportedScore == 7.25, "the double precision column read back as a real");
    CHECK(atomic_load(&reportedOk) == false, "the boolean column read back as a bool");
    CHECK(atomic_load(&reportedBlobLen) == 3, "a bytea param with an embedded NUL round-tripped at full length");
    CHECK(reportedBlob[0] == 'x' && reportedBlob[1] == '\0' && reportedBlob[2] == 'y', "the embedded NUL survived the Postgres bound-param round-trip");

    calogContextClose(ctx);
    calogDestroy(calog);
    calogDbShutdown();

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