// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// Host tests for services/docstore: a JSON document store on the WAL (via dbm), with top-level field
// queries. Exercised over a RAM-backed WalDev; "reboot" = remount the store + reopen the dbm + rebind.

#include "services/dbm/dbm.h"
#include "services/docstore/docstore.h"
#include "services/wal/wal_store.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <unity.h>

void setUp(void)
{
}
void tearDown(void)
{
}

// RAM-backed WalDev.
struct RamDisk
{
    uint8_t *buf;
    uint64_t size;
};
static size_t ram_read(void *ctx, uint64_t off, uint8_t *buf, size_t len)
{
    RamDisk *d = (RamDisk *)ctx;
    if (off + len > d->size)
        return 0;
    memcpy(buf, d->buf + off, len);
    return len;
}
static size_t ram_write(void *ctx, uint64_t off, const uint8_t *buf, size_t len)
{
    RamDisk *d = (RamDisk *)ctx;
    if (off + len > d->size)
        return 0;
    memcpy(d->buf + off, buf, len);
    return len;
}
static bool ram_sync(void *)
{
    return true;
}

static uint8_t g_disk[64 * 1024];
static RamDisk g_d;
static WalDev g_dev;
static WalStore g_wal;
static DetwsDbm g_db;
static DetwsDocStore g_ds;

static WalDev dev_over(RamDisk *d)
{
    WalDev v;
    v.read = ram_read;
    v.write = ram_write;
    v.sync = ram_sync;
    v.ctx = d;
    v.size = d->size;
    return v;
}
static void fresh(void)
{
    g_d.buf = g_disk;
    g_d.size = sizeof(g_disk);
    g_dev = dev_over(&g_d);
    TEST_ASSERT_TRUE(wal_store_format(&g_wal, &g_dev));
    TEST_ASSERT_TRUE(detws_dbm_open(&g_db, &g_wal));
    detws_docstore_open(&g_ds, &g_db);
}
static bool reboot(void)
{
    g_dev = dev_over(&g_d);
    if (!wal_store_mount(&g_wal, &g_dev))
        return false;
    if (!detws_dbm_open(&g_db, &g_wal))
        return false;
    detws_docstore_open(&g_ds, &g_db);
    return true;
}

static bool put_doc(const char *id, const char *json)
{
    return detws_docstore_put(&g_ds, id, (uint16_t)strlen(id), (const uint8_t *)json, (uint32_t)strlen(json));
}
static bool get_eq(const char *id, const char *expect)
{
    uint8_t buf[DETWS_DBM_VAL_MAX + 1];
    long n = detws_docstore_get(&g_ds, id, (uint16_t)strlen(id), buf, sizeof(buf));
    if (n < 0)
        return false;
    return (size_t)n == strlen(expect) && memcmp(buf, expect, n) == 0;
}

// Match callback: collect matched ids into a small set.
struct Collected
{
    int n;
    char ids[8][16];
};
static bool collect(const char *id, uint16_t id_len, const uint8_t *json, uint32_t json_len, void *ctx)
{
    (void)json;
    (void)json_len;
    Collected *c = (Collected *)ctx;
    if (c->n < 8 && id_len < 16)
    {
        memcpy(c->ids[c->n], id, id_len);
        c->ids[c->n][id_len] = 0;
        c->n++;
    }
    return true;
}
static bool has_id(const Collected *c, const char *id)
{
    for (int i = 0; i < c->n; i++)
        if (strcmp(c->ids[i], id) == 0)
            return true;
    return false;
}

void test_put_get_del(void)
{
    fresh();
    TEST_ASSERT_TRUE(put_doc("u1", "{\"name\":\"alice\",\"age\":30,\"admin\":true}"));
    TEST_ASSERT_TRUE(put_doc("u2", "{\"name\":\"bob\",\"age\":25,\"admin\":false}"));
    TEST_ASSERT_TRUE(get_eq("u1", "{\"name\":\"alice\",\"age\":30,\"admin\":true}"));
    TEST_ASSERT_EQUAL_UINT32(2, detws_docstore_count(&g_ds));

    TEST_ASSERT_TRUE(detws_docstore_del(&g_ds, "u2", 2));
    TEST_ASSERT_FALSE(detws_docstore_contains(&g_ds, "u2", 2));
    TEST_ASSERT_EQUAL_UINT32(1, detws_docstore_count(&g_ds));

    // Replace u1's document.
    TEST_ASSERT_TRUE(put_doc("u1", "{\"name\":\"alice2\",\"age\":31}"));
    TEST_ASSERT_TRUE(get_eq("u1", "{\"name\":\"alice2\",\"age\":31}"));
}

void test_find_by_field(void)
{
    fresh();
    put_doc("u1", "{\"name\":\"alice\",\"age\":30,\"city\":\"paris\"}");
    put_doc("u2", "{\"name\":\"bob\",\"age\":30,\"city\":\"paris\"}");
    put_doc("u3", "{\"name\":\"carol\",\"age\":25,\"city\":\"lyon\"}");

    // String field.
    Collected c = {};
    uint32_t m = detws_docstore_find_str(&g_ds, "city", "paris", collect, &c);
    TEST_ASSERT_EQUAL_UINT32(2, m);
    TEST_ASSERT_EQUAL_INT(2, c.n);
    TEST_ASSERT_TRUE(has_id(&c, "u1"));
    TEST_ASSERT_TRUE(has_id(&c, "u2"));
    TEST_ASSERT_FALSE(has_id(&c, "u3"));

    // Integer field.
    Collected c2 = {};
    m = detws_docstore_find_int(&g_ds, "age", 30, collect, &c2);
    TEST_ASSERT_EQUAL_UINT32(2, m);
    TEST_ASSERT_TRUE(has_id(&c2, "u1"));
    TEST_ASSERT_TRUE(has_id(&c2, "u2"));

    // No matches.
    Collected c3 = {};
    m = detws_docstore_find_str(&g_ds, "city", "berlin", collect, &c3);
    TEST_ASSERT_EQUAL_UINT32(0, m);
    TEST_ASSERT_EQUAL_INT(0, c3.n);
}

void test_find_bool(void)
{
    fresh();
    put_doc("a", "{\"on\":true,\"n\":1}");
    put_doc("b", "{\"on\":false,\"n\":2}");
    put_doc("c", "{\"on\":true,\"n\":3}");
    Collected c = {};
    uint32_t m = detws_docstore_find_bool(&g_ds, "on", true, collect, &c);
    TEST_ASSERT_EQUAL_UINT32(2, m);
    TEST_ASSERT_TRUE(has_id(&c, "a"));
    TEST_ASSERT_TRUE(has_id(&c, "c"));
}

void test_persist_and_query_across_reboot(void)
{
    fresh();
    put_doc("u1", "{\"name\":\"alice\",\"role\":\"admin\"}");
    put_doc("u2", "{\"name\":\"bob\",\"role\":\"user\"}");
    put_doc("u3", "{\"name\":\"carol\",\"role\":\"admin\"}");
    TEST_ASSERT_TRUE(detws_docstore_sync(&g_ds));

    TEST_ASSERT_TRUE(reboot());
    TEST_ASSERT_EQUAL_UINT32(3, detws_docstore_count(&g_ds));
    TEST_ASSERT_TRUE(get_eq("u2", "{\"name\":\"bob\",\"role\":\"user\"}"));

    // The field index (JSON scan) works after a remount too.
    Collected c = {};
    uint32_t m = detws_docstore_find_str(&g_ds, "role", "admin", collect, &c);
    TEST_ASSERT_EQUAL_UINT32(2, m);
    TEST_ASSERT_TRUE(has_id(&c, "u1"));
    TEST_ASSERT_TRUE(has_id(&c, "u3"));
}

// find matches only the queried field, and a stop request halts the scan.
void test_find_early_stop(void)
{
    fresh();
    for (int i = 0; i < 5; i++)
    {
        char id[8], doc[48];
        snprintf(id, sizeof(id), "d%d", i);
        snprintf(doc, sizeof(doc), "{\"grp\":\"x\",\"i\":%d}", i);
        put_doc(id, doc);
    }
    // A callback that stops after the first match sees exactly one.
    struct Once
    {
        int seen;
    } once = {0};
    struct L
    {
        static bool cb(const char *, uint16_t, const uint8_t *, uint32_t, void *ctx)
        {
            ((Once *)ctx)->seen++;
            return false; // stop immediately
        }
    };
    uint32_t m = detws_docstore_find_str(&g_ds, "grp", "x", L::cb, &once);
    TEST_ASSERT_EQUAL_UINT32(1, m);
    TEST_ASSERT_EQUAL_INT(1, once.seen);
}

int main(void)
{
    UNITY_BEGIN();
    RUN_TEST(test_put_get_del);
    RUN_TEST(test_find_by_field);
    RUN_TEST(test_find_bool);
    RUN_TEST(test_persist_and_query_across_reboot);
    RUN_TEST(test_find_early_stop);
    return UNITY_END();
}
