// testBroker.c -- leak-checked unit tests for the broker core.
//
// Built with AddressSanitizer + UndefinedBehaviorSanitizer (see Makefile), so a
// clean run also proves there are no leaks, double-frees, or UB. Every test
// allocates and frees through the public API only.

#include "calogInternal.h"

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

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

#define ADD_ARITY       2
#define BULK_REGISTER   100
#define DEPTH_OVERSHOOT 5
#define LIST_ELEMS      3
#define NAME_BUFFER     32

static int32_t testsRun         = 0;
static int32_t testsFailed      = 0;
static int32_t releaseHookCalls = 0;

static void    checkImpl(bool condition, const char *message, const char *file, int32_t line);
static void    countingRelease(CalogFnT *callable);
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBoom(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void    testAggregate(void);
static void    testAggregateCopyCleanup(void);
static void    testAggregateDuplicateKey(void);
static void    testBrokerFailFreesResult(void);
static void    testCallable(void);
static void    testCallableDead(void);
static void    testDepthCap(void);
static void    testMapDepthCap(void);
static void    testRegistry(void);
static void    testScalars(void);
static void    testStringIndependence(void);
static void    testStringNegativeLength(void);
static void    testValueEquals(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 void countingRelease(CalogFnT *callable) {
    (void)callable;
    releaseHookCalls++;
}


static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    (void)userData;
    if (argCount != ADD_ARITY || args[0].type != calogIntE || args[1].type != calogIntE) {
        return calogFail(result, calogErrArgE, "nativeAdd expects two integers");
    }
    calogValueInt(result, args[0].as.i + args[1].as.i);
    return calogOkE;
}


static int32_t nativeBoom(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    (void)args;
    (void)argCount;
    (void)userData;
    return calogFail(result, calogErrArgE, "nativeBoom always fails");
}


static void testAggregate(void) {
    CalogAggT *list;
    CalogAggT *map;
    CalogValueT      container;
    CalogValueT      copy;
    CalogValueT      element;
    CalogValueT      key;
    CalogValueT     *found;
    int32_t     status;
    int64_t     index;

    status = calogAggCreate(&list, calogListE);
    CHECK(status == calogOkE, "list create");
    for (index = 0; index < LIST_ELEMS; index++) {
        calogValueInt(&element, index + 1);
        status = calogAggPush(list, &element);
        CHECK(status == calogOkE, "list push status");
        CHECK(element.type == calogNilE, "push consumes the value");
    }
    calogValueAgg(&container, list);

    status = calogValueCopy(&copy, &container);
    CHECK(status == calogOkE, "list deep copy status");
    calogValueFree(&container);

    CHECK(copy.type == calogAggE, "copy is aggregate");
    CHECK(copy.as.agg->arrayCount == LIST_ELEMS, "copy array count");
    CHECK(copy.as.agg->array[0].as.i == 1, "copy element 0");
    CHECK(copy.as.agg->array[LIST_ELEMS - 1].as.i == LIST_ELEMS, "copy last element");
    calogValueFree(&copy);

    status = calogAggCreate(&map, calogMapE);
    CHECK(status == calogOkE, "map create");
    status = calogValueString(&key, "x", 1);
    CHECK(status == calogOkE, "map key init");
    calogValueBool(&element, true);
    status = calogAggSet(map, &key, &element);
    CHECK(status == calogOkE, "map set status");
    CHECK(key.type == calogNilE && element.type == calogNilE, "set consumes key and value");

    calogValueString(&key, "x", 1);
    found = calogAggGet(map, &key);
    calogValueFree(&key);
    CHECK(found != NULL && found->type == calogBoolE && found->as.b == true, "map get hit");

    calogValueString(&key, "missing", 7);
    found = calogAggGet(map, &key);
    calogValueFree(&key);
    CHECK(found == NULL, "map get miss");

    calogValueAgg(&container, map);
    calogValueFree(&container);
}


static void testAggregateCopyCleanup(void) {
    CalogFnT  *callable;
    CalogAggT *list;
    CalogValueT      container;
    CalogValueT      copy;
    CalogValueT      element;
    CalogValueT      deepValue;
    int32_t     status;
    int32_t     level;

    releaseHookCalls = 0;
    status = calogFnCreate(&callable, NULL, nativeAdd, NULL, countingRelease, 0);
    CHECK(status == calogOkE, "cleanup callable create");

    status = calogAggCreate(&list, calogListE);
    CHECK(status == calogOkE, "cleanup list create");

    calogValueString(&element, "sibling", 7);
    calogAggPush(list, &element);

    calogValueFn(&element, callable);
    calogAggPush(list, &element);

    calogValueInt(&deepValue, 0);
    for (level = 0; level < CALOG_MAX_DEPTH + DEPTH_OVERSHOOT; level++) {
        CalogAggT *wrapper;
        calogAggCreate(&wrapper, calogListE);
        calogAggPush(wrapper, &deepValue);
        calogValueAgg(&deepValue, wrapper);
    }
    calogAggPush(list, &deepValue);

    calogValueAgg(&container, list);
    status = calogValueCopy(&copy, &container);
    CHECK(status == calogErrDepthE, "cleanup copy hits depth cap");
    CHECK(copy.type == calogNilE, "cleanup copy left nil");
    CHECK(releaseHookCalls == 0, "failed copy released its retained ref, original intact");

    calogValueFree(&container);
    CHECK(releaseHookCalls == 1, "callable released once when original list freed");
    calogValueFree(&copy);
}


static void testAggregateDuplicateKey(void) {
    CalogAggT *map;
    CalogValueT      key;
    CalogValueT      value;
    CalogValueT     *found;
    int32_t     status;

    status = calogAggCreate(&map, calogMapE);
    CHECK(status == calogOkE, "dup map create");

    calogValueString(&key, "k", 1);
    calogValueInt(&value, 1);
    status = calogAggSet(map, &key, &value);
    CHECK(status == calogOkE, "dup first set");

    calogValueString(&key, "k", 1);
    calogValueInt(&value, 2);
    status = calogAggSet(map, &key, &value);
    CHECK(status == calogOkE, "dup second set");
    CHECK(key.type == calogNilE && value.type == calogNilE, "dup set consumes key and value");
    CHECK(map->pairCount == 1, "duplicate key did not add a pair");

    calogValueString(&key, "k", 1);
    found = calogAggGet(map, &key);
    calogValueFree(&key);
    CHECK(found != NULL && found->type == calogIntE && found->as.i == 2, "duplicate key replaced value");

    calogAggFree(map);
}


static void testBrokerFailFreesResult(void) {
    CalogAggT *aggregate;
    CalogValueT      result;
    CalogValueT      element;
    int32_t     status;

    status = calogAggCreate(&aggregate, calogListE);
    CHECK(status == calogOkE, "fail-frees aggregate create");
    calogValueInt(&element, 1);
    calogAggPush(aggregate, &element);
    calogValueAgg(&result, aggregate);

    status = calogFail(&result, calogErrArgE, "boom");
    CHECK(status == calogErrArgE, "calogFail returns the status");
    CHECK(result.type == calogStringE, "calogFail freed the owning result and set an error string");
    calogValueFree(&result);
}


static void testCallable(void) {
    CalogFnT *callable;
    CalogValueT     handle;
    CalogValueT     copyA;
    CalogValueT     copyB;
    CalogValueT     args[ADD_ARITY];
    CalogValueT     result;
    int32_t    status;

    releaseHookCalls = 0;

    status = calogFnCreate(&callable, NULL, nativeAdd, NULL, countingRelease, 0);
    CHECK(status == calogOkE, "callable create");
    calogValueFn(&handle, callable);

    status = calogValueCopy(&copyA, &handle);
    CHECK(status == calogOkE, "callable copy A");
    status = calogValueCopy(&copyB, &copyA);
    CHECK(status == calogOkE, "callable copy B");

    calogValueInt(&args[0], 20);
    calogValueInt(&args[1], 22);
    status = calogFnInvoke(callable, args, ADD_ARITY, &result);
    CHECK(status == calogOkE, "callable invoke status");
    CHECK(result.type == calogIntE && result.as.i == 42, "callable invoke result");
    calogValueFree(&result);
    calogValueFree(&args[0]);
    calogValueFree(&args[1]);

    calogValueFree(&handle);
    calogValueFree(&copyA);
    CHECK(releaseHookCalls == 0, "release hook not called while references remain");
    calogValueFree(&copyB);
    CHECK(releaseHookCalls == 1, "release hook called once on last drop");
}


static void testCallableDead(void) {
    CalogFnT *callable;
    CalogValueT     handle;
    CalogValueT     result;
    int32_t    status;

    releaseHookCalls = 0;
    status = calogFnCreate(&callable, NULL, nativeAdd, NULL, countingRelease, 0);
    CHECK(status == calogOkE, "dead callable create");
    calogValueFn(&handle, callable);

    calogFnMarkDead(callable);
    status = calogFnInvoke(callable, NULL, 0, &result);
    CHECK(status == calogErrNotFoundE, "invoke on dead callable returns not-found");
    CHECK(result.type == calogStringE, "dead invoke leaves an error string");
    calogValueFree(&result);

    calogValueFree(&handle);
    CHECK(releaseHookCalls == 1, "dead callable still released on last drop");
}


static void testDepthCap(void) {
    CalogValueT  current;
    CalogValueT  copy;
    int32_t status;
    int32_t level;
    int32_t depth;

    depth = CALOG_MAX_DEPTH + DEPTH_OVERSHOOT;
    calogValueInt(&current, 7);
    for (level = 0; level < depth; level++) {
        CalogAggT *wrapper;
        status = calogAggCreate(&wrapper, calogListE);
        CHECK(status == calogOkE, "depth wrapper create");
        status = calogAggPush(wrapper, &current);
        CHECK(status == calogOkE, "depth wrapper push");
        calogValueAgg(&current, wrapper);
    }
    status = calogValueCopy(&copy, &current);
    CHECK(status == calogErrDepthE, "deep copy rejected by depth cap");
    CHECK(copy.type == calogNilE, "rejected copy left nil");
    calogValueFree(&current);
    calogValueFree(&copy);
}


static void testMapDepthCap(void) {
    CalogAggT *map;
    CalogValueT      container;
    CalogValueT      copy;
    CalogValueT      key;
    CalogValueT      value;
    CalogValueT      deepValue;
    int32_t     status;
    int32_t     level;

    status = calogAggCreate(&map, calogMapE);
    CHECK(status == calogOkE, "map depth create");

    calogValueString(&key, "shallow", 7);
    calogValueInt(&value, 1);
    calogAggSet(map, &key, &value);

    calogValueInt(&deepValue, 0);
    for (level = 0; level < CALOG_MAX_DEPTH + DEPTH_OVERSHOOT; level++) {
        CalogAggT *wrapper;
        calogAggCreate(&wrapper, calogListE);
        calogAggPush(wrapper, &deepValue);
        calogValueAgg(&deepValue, wrapper);
    }
    calogValueString(&key, "deep", 4);
    calogAggSet(map, &key, &deepValue);

    calogValueAgg(&container, map);
    status = calogValueCopy(&copy, &container);
    CHECK(status == calogErrDepthE, "map deep copy hits the depth cap");
    CHECK(copy.type == calogNilE, "map deep copy left nil");
    calogValueFree(&container);
    calogValueFree(&copy);
}


static void testRegistry(void) {
    CalogT *broker;
    CalogValueT   args[ADD_ARITY];
    CalogValueT   result;
    char     name[NAME_BUFFER];
    int32_t  status;
    int32_t  index;

    broker = calogBrokerCreate();
    CHECK(broker != NULL, "broker create");

    status = calogRegister(broker, "add", nativeAdd, NULL);
    CHECK(status == calogOkE, "register add");
    status = calogRegister(broker, "boom", nativeBoom, NULL);
    CHECK(status == calogOkE, "register boom");

    for (index = 0; index < BULK_REGISTER; index++) {
        snprintf(name, sizeof(name), "fn%d", index);
        status = calogRegister(broker, name, nativeAdd, NULL);
        CHECK(status == calogOkE, "register bulk");
    }
    CHECK(calogLookup(broker, "fn50") != NULL, "bulk lookup hit");
    CHECK(calogLookup(broker, "nope") == NULL, "lookup miss");

    // Re-registration must replace in place without adding a slot.
    status = calogRegister(broker, "add", nativeBoom, NULL);
    CHECK(status == calogOkE, "re-register add");

    calogValueInt(&args[0], 3);
    calogValueInt(&args[1], 4);
    status = calogCall(broker, "boom", args, ADD_ARITY, &result);
    CHECK(status == calogErrArgE, "re-registered add now boom");
    calogValueFree(&result);
    status = calogRegister(broker, "add", nativeAdd, NULL);
    CHECK(status == calogOkE, "restore add");

    status = calogCall(broker, "add", args, ADD_ARITY, &result);
    CHECK(status == calogOkE, "call add status");
    CHECK(result.type == calogIntE && result.as.i == 7, "call add result");
    calogValueFree(&result);
    calogValueFree(&args[0]);
    calogValueFree(&args[1]);

    status = calogCall(broker, "missing", NULL, 0, &result);
    CHECK(status == calogErrNotFoundE, "call missing status");
    CHECK(result.type == calogStringE, "call missing leaves error string");
    calogValueFree(&result);

    status = calogCall(broker, "boom", NULL, 0, &result);
    CHECK(status == calogErrArgE, "call boom status");
    CHECK(result.type == calogStringE, "call boom leaves error string");
    calogValueFree(&result);

    calogBrokerDestroy(broker);
}


static void testScalars(void) {
    CalogValueT  source;
    CalogValueT  copy;
    int32_t status;

    calogValueInt(&source, 42);
    status = calogValueCopy(&copy, &source);
    CHECK(status == calogOkE, "scalar int copy status");
    CHECK(copy.type == calogIntE && copy.as.i == 42, "scalar int copy value");
    calogValueFree(&source);
    calogValueFree(&copy);

    calogValueReal(&source, 3.5);
    status = calogValueCopy(&copy, &source);
    CHECK(status == calogOkE, "scalar real copy status");
    CHECK(copy.type == calogRealE && copy.as.r == 3.5, "scalar real copy value");
    calogValueFree(&source);
    calogValueFree(&copy);

    calogValueBool(&source, true);
    status = calogValueCopy(&copy, &source);
    CHECK(status == calogOkE, "scalar bool copy status");
    CHECK(copy.type == calogBoolE && copy.as.b == true, "scalar bool copy value");
    calogValueFree(&source);
    calogValueFree(&copy);

    calogValueNil(&source);
    CHECK(source.type == calogNilE, "nil init");
    calogValueFree(&source);
}


static void testStringIndependence(void) {
    CalogValueT     source;
    CalogValueT     copy;
    int32_t    status;
    const char embedded[3] = { 'a', '\0', 'b' };

    status = calogValueString(&source, "hello", 5);
    CHECK(status == calogOkE, "string init status");
    status = calogValueCopy(&copy, &source);
    CHECK(status == calogOkE, "string copy status");
    CHECK(copy.as.s.length == 5, "string copy length");
    CHECK(copy.as.s.bytes != source.as.s.bytes, "string copy is a distinct buffer");
    CHECK(memcmp(copy.as.s.bytes, "hello", 5) == 0, "string copy content");
    calogValueFree(&source);
    CHECK(memcmp(copy.as.s.bytes, "hello", 5) == 0, "string copy survives source free");
    calogValueFree(&copy);

    status = calogValueString(&source, embedded, 3);
    CHECK(status == calogOkE, "embedded-NUL init status");
    CHECK(source.as.s.length == 3, "embedded-NUL length preserved");
    status = calogValueCopy(&copy, &source);
    CHECK(status == calogOkE, "embedded-NUL copy status");
    CHECK(copy.as.s.length == 3 && memcmp(copy.as.s.bytes, embedded, 3) == 0, "embedded-NUL copy content");
    calogValueFree(&source);
    calogValueFree(&copy);
}


static void testStringNegativeLength(void) {
    CalogValueT  value;
    int32_t status;

    status = calogValueString(&value, "x", -1);
    CHECK(status == calogErrRangeE, "negative length rejected");
    CHECK(value.type == calogNilE, "negative length leaves nil");
    calogValueFree(&value);

    status = calogValueString(&value, NULL, 5);
    CHECK(status == calogErrArgE, "null bytes with positive length rejected");
    CHECK(value.type == calogNilE, "null bytes leaves nil");
    calogValueFree(&value);

    status = calogValueString(&value, NULL, 0);
    CHECK(status == calogOkE, "empty string from null bytes ok");
    CHECK(value.type == calogStringE && value.as.s.length == 0, "empty string created");
    calogValueFree(&value);
}


static void testValueEquals(void) {
    CalogValueT a;
    CalogValueT b;

    calogValueInt(&a, 5);
    calogValueInt(&b, 5);
    CHECK(calogValueEquals(&a, &b), "int equal");
    calogValueFree(&a);
    calogValueFree(&b);

    calogValueInt(&a, 5);
    calogValueInt(&b, 6);
    CHECK(!calogValueEquals(&a, &b), "int not equal");
    calogValueFree(&a);
    calogValueFree(&b);

    calogValueInt(&a, 5);
    calogValueReal(&b, 5.0);
    CHECK(!calogValueEquals(&a, &b), "type mismatch not equal");
    calogValueFree(&a);
    calogValueFree(&b);

    calogValueString(&a, "hi", 2);
    calogValueString(&b, "hi", 2);
    CHECK(calogValueEquals(&a, &b), "string equal");
    calogValueFree(&a);
    calogValueFree(&b);

    calogValueString(&a, "hi", 2);
    calogValueString(&b, "ho", 2);
    CHECK(!calogValueEquals(&a, &b), "string not equal");
    calogValueFree(&a);
    calogValueFree(&b);

    calogValueNil(&a);
    calogValueNil(&b);
    CHECK(calogValueEquals(&a, &b), "nil equal");
    calogValueFree(&a);
    calogValueFree(&b);
}


int main(void) {
    testAggregate();
    testAggregateCopyCleanup();
    testAggregateDuplicateKey();
    testBrokerFailFreesResult();
    testCallable();
    testCallableDead();
    testDepthCap();
    testMapDepthCap();
    testRegistry();
    testScalars();
    testStringIndependence();
    testStringNegativeLength();
    testValueEquals();

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