/* test_store.c — M1 gate for the engram paged store (engram_store.{c,h}).
 *
 * Pure C. Build:  gcc -O2 test_store.c ../../lang/runtime/engram_store.c -o test_store
 * Writes ONLY under a throwaway /tmp dir. Never touches ~/.neuron or live ports.
 *
 * Covers §7 M1 gates: round-trip (5k nodes / 20k edges, all fields, emb bit-exact,
 * hebb, >page content), TLV forward-compat, overflow chains, B+-tree indexes
 * across splits, free-list reuse, and corruption/superblock recovery.
 */
#include "../../lang/runtime/engram_store.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

static int g_pass = 0, g_fail = 0;
static void ok(const char* name, int cond){
    printf("  [%s] %s\n", cond ? "PASS" : "FAIL", name);
    if (cond) g_pass++; else g_fail++;
}

static char g_dir[512];
static void mk_dir(void){
    snprintf(g_dir, sizeof g_dir, "/tmp/engram-store-test-%d", (int)getpid());
    mkdir(g_dir, 0700);
}
static void path_in(char* out, size_t cap, const char* name){
    snprintf(out, cap, "%s/%s", g_dir, name);
}
static long file_size(const char* p){ struct stat st; return stat(p,&st)==0 ? (long)st.st_size : -1; }

/* ── deterministic RNG so oracle nodes/edges regenerate bit-exact ─────────── */
static uint64_t xs(uint64_t* s){ uint64_t x=*s; x^=x<<13; x^=x>>7; x^=x<<17; *s=x; return x; }
static uint64_t node_seed(int i){ return 0x9E3779B97F4A7C15ULL ^ ((uint64_t)(i+1)*0xD1B54A32D192ED03ULL); }
static uint64_t edge_seed(int i){ return 0xC2B2AE3D27D4EB4FULL ^ ((uint64_t)(i+1)*0x165667B19E3779F9ULL); }

static char* rnd_str(uint64_t* st, size_t len){
    char* s = (char*)malloc(len + 1);
    for (size_t i=0;i<len;i++) s[i] = (char)(33 + (xs(st) % 94)); /* printable, no NUL */
    s[len] = 0; return s;
}

/* NODE_COUNT nodes; a slice have >page content to force overflow chains. */
#define NODE_COUNT 5000
#define EDGE_COUNT 20000
#define EMB_DIM    768

static void gen_node(int i, StoreNode* n){
    memset(n, 0, sizeof *n);
    uint64_t st = node_seed(i);
    char id[32]; snprintf(id, sizeof id, "node-%d", i);
    n->id = strdup(id);
    size_t clen = (i % 500 == 0) ? (size_t)(17000 + (xs(&st) % 6000)) : (size_t)(xs(&st) % 300);
    n->content   = rnd_str(&st, clen);
    n->node_type = rnd_str(&st, 4 + (xs(&st) % 8));
    n->label     = (i % 2)   ? rnd_str(&st, 3 + (xs(&st) % 10)) : NULL;
    n->tier      = rnd_str(&st, 4 + (xs(&st) % 6));
    n->tags      = rnd_str(&st, xs(&st) % 40);
    n->metadata  = (i % 3)   ? rnd_str(&st, xs(&st) % 60) : NULL;
    n->salience             = (double)(xs(&st) % 1000000) / 997.0;
    n->importance           = (double)(xs(&st) % 1000000) / 131.0;
    n->confidence           = (double)(xs(&st) % 1000000) / 733.0;
    n->temporal_decay_rate  = (double)(xs(&st) % 1000000) / 101.0;
    n->activation_count     = (int64_t)(xs(&st) % 100000);
    n->last_activated       = (int64_t)xs(&st);
    n->created_at           = (int64_t)(1600000000000LL + i);
    n->updated_at           = (int64_t)xs(&st);
    n->background_activation = (double)(xs(&st) % 1000000) / 17.0;
    n->working_memory_weight = (double)(xs(&st) % 1000000) / 29.0;
    n->suppression_count    = (int32_t)(xs(&st) % 50);
    n->layer_id             = (uint32_t)(xs(&st) % 5);
    for (int k=0;k<STORE_BLL_K;k++) n->access_ts[k] = (int64_t)xs(&st);
    n->access_head   = (int32_t)(xs(&st) % STORE_BLL_K);
    n->access_filled = (int32_t)(xs(&st) % (STORE_BLL_K + 1));
    n->wm_anchor     = (double)(xs(&st) % 1000000) / 3.0;
    n->emb = (float*)malloc(EMB_DIM * sizeof(float));
    for (int k=0;k<EMB_DIM;k++){ uint32_t u=(uint32_t)xs(&st); memcpy(&n->emb[k], &u, 4); }
    n->emb_dim = EMB_DIM;
}

static void gen_edge(int i, StoreEdge* e){
    memset(e, 0, sizeof *e);
    uint64_t st = edge_seed(i);
    char id[32], from[32], to[32];
    snprintf(id, sizeof id, "edge-%d", i);
    snprintf(from, sizeof from, "node-%d", (int)(xs(&st) % NODE_COUNT));
    snprintf(to,   sizeof to,   "node-%d", (int)(xs(&st) % NODE_COUNT));
    e->id = strdup(id); e->from_id = strdup(from); e->to_id = strdup(to);
    e->relation = rnd_str(&st, 3 + (xs(&st) % 12));
    e->metadata = (i % 4) ? rnd_str(&st, xs(&st) % 40) : NULL;
    e->weight     = (double)(xs(&st) % 1000000) / 111.0;
    e->hebb       = (double)(xs(&st) % 1000000) / 1000000.0; /* the learned field */
    e->confidence = (double)(xs(&st) % 1000000) / 777.0;
    e->created_at = (int64_t)(1600000000000LL + i);
    e->updated_at = (int64_t)xs(&st);
    e->last_fired = (int64_t)xs(&st);
    e->inhibitory = (int32_t)(xs(&st) % 2);
    e->layer_id   = (uint32_t)(xs(&st) % 5);
}

static int streq(const char* a, const char* b){
    if (!a && !b) return 1;
    if (!a || !b) return 0;
    return strcmp(a,b)==0;
}
static int cmp_node(const StoreNode* a, const StoreNode* b){
    if (!streq(a->id,b->id) || !streq(a->content,b->content) ||
        !streq(a->node_type,b->node_type) || !streq(a->label,b->label) ||
        !streq(a->tier,b->tier) || !streq(a->tags,b->tags) ||
        !streq(a->metadata,b->metadata)) return 0;
    if (a->salience!=b->salience || a->importance!=b->importance ||
        a->confidence!=b->confidence || a->temporal_decay_rate!=b->temporal_decay_rate ||
        a->activation_count!=b->activation_count || a->last_activated!=b->last_activated ||
        a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
        a->background_activation!=b->background_activation ||
        a->working_memory_weight!=b->working_memory_weight ||
        a->suppression_count!=b->suppression_count || a->layer_id!=b->layer_id ||
        a->access_head!=b->access_head || a->access_filled!=b->access_filled ||
        a->wm_anchor!=b->wm_anchor || a->emb_dim!=b->emb_dim) return 0;
    for (int k=0;k<STORE_BLL_K;k++) if (a->access_ts[k]!=b->access_ts[k]) return 0;
    if ((a->emb==NULL) != (b->emb==NULL)) return 0;
    if (a->emb && memcmp(a->emb, b->emb, (size_t)a->emb_dim*4)!=0) return 0;
    return 1;
}
static int cmp_edge(const StoreEdge* a, const StoreEdge* b){
    if (!streq(a->id,b->id) || !streq(a->from_id,b->from_id) || !streq(a->to_id,b->to_id) ||
        !streq(a->relation,b->relation) || !streq(a->metadata,b->metadata)) return 0;
    if (a->weight!=b->weight || a->hebb!=b->hebb || a->confidence!=b->confidence ||
        a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
        a->last_fired!=b->last_fired || a->inhibitory!=b->inhibitory ||
        a->layer_id!=b->layer_id) return 0;
    return 1;
}
static void free_node_fields(StoreNode* n){
    free(n->id); free(n->content); free(n->node_type); free(n->label);
    free(n->tier); free(n->tags); free(n->metadata); free(n->emb); free(n->unknown);
}
static void free_edge_fields(StoreEdge* e){
    free(e->id); free(e->from_id); free(e->to_id); free(e->relation); free(e->metadata); free(e->unknown);
}

/* Flip one byte in the store file at (page*PAGE_SIZE + off). */
static void flip_byte(const char* path, uint64_t page, size_t off){
    int fd = open(path, O_RDWR);
    uint8_t b; off_t at = (off_t)page*STORE_PAGE_SIZE + off;
    pread(fd, &b, 1, at); b ^= 0xFF; pwrite(fd, &b, 1, at); close(fd);
}

/* ════════════════════════════════════════════════════════════════════════ */

static void test_roundtrip(void){
    printf("\n== round-trip: %d nodes + %d edges, all fields, emb bit-exact ==\n", NODE_COUNT, EDGE_COUNT);
    char path[600]; path_in(path, sizeof path, "roundtrip.store");
    unlink(path);
    EngramPagedStore* s = store_create(path);
    ok("store_create", s != NULL);
    if (!s) return;

    for (int i=0;i<NODE_COUNT;i++){ StoreNode n; gen_node(i,&n);
        if (store_put_node(s,&n)!=0){ ok("put_node", 0); free_node_fields(&n); store_close(s); return; }
        free_node_fields(&n); }
    for (int i=0;i<EDGE_COUNT;i++){ StoreEdge e; gen_edge(i,&e);
        if (store_put_edge(s,&e)!=0){ ok("put_edge", 0); free_edge_fields(&e); store_close(s); return; }
        free_edge_fields(&e); }
    ok("wrote all nodes+edges", 1);
    store_close(s);

    long sz = file_size(path);
    printf("  store file size: %ld bytes (%.2f MB) for %d nodes / %d edges\n",
           sz, sz/1048576.0, NODE_COUNT, EDGE_COUNT);

    s = store_open(path);
    ok("store_open (reopen)", s != NULL);
    if (!s) return;

    int nbad = 0;
    for (int i=0;i<NODE_COUNT;i++){
        StoreNode want; gen_node(i,&want);
        StoreNode got; int r = store_get_node(s, want.id, &got);
        if (r!=1 || !cmp_node(&want,&got) || got.unknown_len!=0) nbad++;
        if (r==1) store_node_free(&got);
        free_node_fields(&want);
    }
    ok("all 5000 nodes read back bit-exact (incl emb, all fields)", nbad==0);
    if (nbad) printf("     %d node mismatches\n", nbad);

    int ebad = 0;
    for (int i=0;i<EDGE_COUNT;i++){
        StoreEdge want; gen_edge(i,&want);
        StoreEdge* got; size_t gn;
        int found = 0;
        if (store_get_edges_from(s, want.from_id, &got, &gn)==0){
            for (size_t j=0;j<gn;j++) if (streq(got[j].id, want.id)){ if (cmp_edge(&want,&got[j])) found=1; break; }
            store_edges_free(got, gn);
        }
        if (!found) ebad++;
        free_edge_fields(&want);
    }
    ok("all 20000 edges read back via adjacency, all fields incl hebb", ebad==0);
    if (ebad) printf("     %d edge mismatches\n", ebad);

    ok("store_check crc clean after round-trip", store_check(s, STORE_CHECK_CRC)==0);
    store_close(s);
}

static void test_forward_compat(void){
    printf("\n== TLV forward-compat: omit field defaults; unknown tag preserved ==\n");
    char path[600]; path_in(path, sizeof path, "fwd.store");
    unlink(path);
    EngramPagedStore* s = store_create(path);

    /* Writer OMITS several fields (metadata, label, emb) → reader must default. */
    StoreNode a; memset(&a,0,sizeof a);
    a.id = strdup("omit-1"); a.content = strdup("has content"); a.tier = strdup("core");
    a.salience = 0.5;                    /* metadata/label NULL, emb NULL */
    store_put_node(s, &a); free(a.id); free(a.content); free(a.tier);

    StoreNode g; int r = store_get_node(s, "omit-1", &g);
    ok("omitted string fields default to NULL", r==1 && g.metadata==NULL && g.label==NULL);
    ok("omitted emb defaults to NULL / emb_dim 0", r==1 && g.emb==NULL && g.emb_dim==0);
    ok("present fields intact", r==1 && streq(g.content,"has content") && g.salience==0.5);
    if (r==1) store_node_free(&g);

    /* Writer includes an UNKNOWN tag (simulating a newer writer / field the
     * reader does not model) via the `unknown` passthrough. Reader (which also
     * models known fields A,B,C) must preserve it verbatim. */
    uint8_t unk[64];
    unk[0] = 200;                        /* a tag this build has no case for */
    /* [u8 tag][u32 len][bytes] */
    unk[1]=8; unk[2]=0; unk[3]=0; unk[4]=0;
    for (int i=0;i<8;i++) unk[5+i] = (uint8_t)(0xA0 + i);
    StoreNode b; memset(&b,0,sizeof b);
    b.id = strdup("unk-1"); b.content = strdup("known field B"); b.confidence = 0.9; /* known field C-ish */
    b.unknown = unk; b.unknown_len = 5 + 8;
    store_put_node(s, &b); free(b.id); free(b.content);

    StoreNode g2; int r2 = store_get_node(s, "unk-1", &g2);
    int unk_ok = r2==1 && g2.unknown_len==(5+8) && memcmp(g2.unknown, unk, 5+8)==0;
    ok("unknown tag preserved verbatim on read", unk_ok);
    ok("known fields still read while unknown preserved", r2==1 && streq(g2.content,"known field B") && g2.confidence==0.9);
    if (r2==1) store_node_free(&g2);

    store_close(s);
}

static void test_overflow(void){
    printf("\n== overflow: 100KB content node + emb via overflow chain ==\n");
    char path[600]; path_in(path, sizeof path, "ovf.store");
    unlink(path);
    EngramPagedStore* s = store_create(path);

    size_t big = 100*1024;
    StoreNode n; memset(&n,0,sizeof n);
    n.id = strdup("big-1");
    n.content = (char*)malloc(big+1);
    for (size_t i=0;i<big;i++) n.content[i] = (char)(33 + (i % 94));
    n.content[big] = 0;
    n.tier = strdup("episodic");
    n.emb = (float*)malloc(EMB_DIM*sizeof(float));
    for (int k=0;k<EMB_DIM;k++){ float f = (float)(k*0.5 - 100.0); n.emb[k]=f; }
    n.emb_dim = EMB_DIM;
    ok("put 100KB+emb node", store_put_node(s,&n)==0);
    store_close(s);

    s = store_open(path);
    StoreNode g; int r = store_get_node(s, "big-1", &g);
    ok("reopen + read big node", r==1);
    ok("100KB content byte-exact via overflow", r==1 && strlen(g.content)==big && memcmp(g.content,n.content,big)==0);
    ok("emb bit-exact via overflow record", r==1 && g.emb_dim==EMB_DIM && memcmp(g.emb,n.emb,EMB_DIM*4)==0);
    if (r==1) store_node_free(&g);
    ok("store_check clean (overflow pages crc'd)", store_check(s, STORE_CHECK_CRC)==0);
    store_close(s);
    free_node_fields(&n);
}

static void test_index_splits(void){
    printf("\n== B+-tree index correctness across many splits ==\n");
    char path[600]; path_in(path, sizeof path, "idx.store");
    unlink(path);
    EngramPagedStore* s = store_create(path);
    /* Tiny order forces deep leaf + internal splits with only a few hundred keys. */
    store__set_btree_order(s, 4, 4);

    const int N = 600;
    for (int i=0;i<N;i++){
        StoreNode n; memset(&n,0,sizeof n);
        char id[32]; snprintf(id,sizeof id,"k-%05d", (i*37+11)%100000); /* scattered keys */
        n.id = strdup(id); n.content = strdup("x"); n.tier=strdup("t"); n.salience=i;
        if (store_put_node(s,&n)!=0){ ok("put",0); }
        free(n.id); free(n.content); free(n.tier);
    }
    int miss=0;
    for (int i=0;i<N;i++){
        char id[32]; snprintf(id,sizeof id,"k-%05d",(i*37+11)%100000);
        StoreNode g; int r = store_get_node(s, id, &g);
        if (r!=1 || (int)g.salience != i) miss++;
        if (r==1) store_node_free(&g);
    }
    ok("all keys retrievable after leaf+internal splits", miss==0);
    if (miss) printf("     %d misses\n", miss);
    StoreNode g; ok("absent key returns 0", store_get_node(s,"k-NOPE",&g)==0);

    /* Adjacency: controlled star + chain, exact edge sets. */
    for (int i=0;i<50;i++){
        StoreEdge e; memset(&e,0,sizeof e);
        char id[32]; snprintf(id,sizeof id,"e-%d",i);
        e.id=strdup(id); e.from_id=strdup("HUB"); char tt[16]; snprintf(tt,sizeof tt,"T-%d",i); e.to_id=strdup(tt);
        e.relation=strdup("r"); e.weight=1.0; e.hebb=0.1*i;
        store_put_edge(s,&e); free_edge_fields(&e);
    }
    for (int i=0;i<7;i++){
        StoreEdge e; memset(&e,0,sizeof e);
        char id[32]; snprintf(id,sizeof id,"in-%d",i);
        char ff[16]; snprintf(ff,sizeof ff,"S-%d",i);
        e.id=strdup(id); e.from_id=strdup(ff); e.to_id=strdup("SINK");
        e.relation=strdup("r"); e.weight=1.0;
        store_put_edge(s,&e); free_edge_fields(&e);
    }
    StoreEdge* out; size_t on;
    store_get_edges_from(s,"HUB",&out,&on);
    ok("get_edges_from(HUB) == 50", on==50);
    store_edges_free(out,on);
    store_get_edges_to(s,"SINK",&out,&on);
    ok("get_edges_to(SINK) == 7", on==7);
    store_edges_free(out,on);
    store_get_edges_to(s,"HUB",&out,&on);
    ok("get_edges_to(HUB) == 0 (direction separation)", on==0);
    store_edges_free(out,on);

    ok("store_check clean", store_check(s, STORE_CHECK_CRC)==0);
    store_close(s);
}

static void test_freelist(void){
    printf("\n== free-list: tombstone reclaims pages, graph stays consistent ==\n");
    char path[600]; path_in(path, sizeof path, "free.store");
    unlink(path);
    EngramPagedStore* s = store_create(path);

    uint64_t pc0 = store_page_count(s);
    const int N = 300;
    for (int i=0;i<N;i++){
        StoreNode n; memset(&n,0,sizeof n);
        char id[32]; snprintf(id,sizeof id,"a-%d",i);
        n.id=strdup(id); n.content=rnd_str(&(uint64_t){node_seed(i)}, 200); n.tier=strdup("t");
        store_put_node(s,&n); free_node_fields(&n);
    }
    uint64_t pc1 = store_page_count(s);
    uint64_t node_pages = pc1 - pc0;
    ok("initial batch consumed pages", node_pages > 0);

    for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"a-%d",i); store_tombstone(s,id); }
    /* all old nodes gone */
    int gone=1; for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"a-%d",i);
        StoreNode g; if (store_get_node(s,id,&g)==1){ gone=0; store_node_free(&g); } }
    ok("tombstoned nodes now absent", gone);

    for (int i=0;i<N;i++){
        StoreNode n; memset(&n,0,sizeof n);
        char id[32]; snprintf(id,sizeof id,"b-%d",i);
        n.id=strdup(id); n.content=strdup("reused"); n.tier=strdup("t"); n.salience=i;
        store_put_node(s,&n); free_node_fields(&n);
    }
    uint64_t pc2 = store_page_count(s);
    /* Reuse proven: growth for the 2nd batch is far less than a fresh alloc. */
    ok("freed pages reused (no full re-growth)", pc2 < pc1 + node_pages);
    printf("     pages: base=%llu after1=%llu after2=%llu (node_pages=%llu)\n",
           (unsigned long long)pc0,(unsigned long long)pc1,(unsigned long long)pc2,(unsigned long long)node_pages);

    int newbad=0; for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"b-%d",i);
        StoreNode g; if (store_get_node(s,id,&g)!=1 || (int)g.salience!=i) newbad++; else store_node_free(&g); }
    ok("new batch fully readable after reuse", newbad==0);
    ok("store_check clean after reuse", store_check(s, STORE_CHECK_CRC)==0);

    store_close(s);
    /* survives reopen */
    s = store_open(path);
    int rb=0; for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"b-%d",i);
        StoreNode g; if (store_get_node(s,id,&g)!=1) rb++; else store_node_free(&g); }
    ok("graph consistent across reopen after reuse", rb==0);
    store_close(s);
}

static void test_corruption(void){
    printf("\n== corruption: crc detection + superblock mirror recovery ==\n");
    char path[600]; path_in(path, sizeof path, "corrupt.store");
    unlink(path);
    EngramPagedStore* s = store_create(path);
    for (int i=0;i<50;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); free_node_fields(&n); }
    store_close(s);

    s = store_open(path);
    ok("clean store: store_check == 0", store_check(s, STORE_CHECK_CRC)==0);
    store_close(s);

    /* flip a byte inside a data page (page 5 is node/index data, never a SB) */
    flip_byte(path, 5, 137);
    s = store_open(path);
    ok("store_open still succeeds (data-page corruption)", s != NULL);
    int bad = store_check(s, STORE_CHECK_CRC);
    ok("store_check detects corrupted page via crc", bad >= 1);
    printf("     store_check reported %d corrupt page(s)\n", bad);
    store_close(s);

    /* fresh store, corrupt superblock 0, must recover via mirror superblock 1 */
    char p2[600]; path_in(p2, sizeof p2, "sbrec.store");
    unlink(p2);
    s = store_create(p2);
    StoreNode n; gen_node(42,&n); store_put_node(s,&n);
    store_close(s);
    /* trash magic + crc region of page 0 */
    flip_byte(p2, 0, 0); flip_byte(p2, 0, 1); flip_byte(p2, 0, 90);
    s = store_open(p2);
    ok("open recovers via mirror superblock (page 1)", s != NULL);
    if (s){
        StoreNode g; int r = store_get_node(s, "node-42", &g);
        ok("data intact after superblock recovery", r==1 && cmp_node(&n,&g));
        if (r==1) store_node_free(&g);
        store_close(s);
    }
    free_node_fields(&n);
}

int main(void){
    mk_dir();
    printf("engram_store M1 test harness — dir=%s\n", g_dir);
    test_roundtrip();
    test_forward_compat();
    test_overflow();
    test_index_splits();
    test_freelist();
    test_corruption();
    printf("\n================  %d passed, %d failed  ================\n", g_pass, g_fail);
    return g_fail ? 1 : 0;
}
