## Schema persistence — CREATE TABLE / data survive reopen
import std/unittest
import std/os
import std/strutils
import std/tables
import barabadb/storage/lsm
import barabadb/storage/btree
import barabadb/query/executor
import barabadb/query/parser
import barabadb/fts/engine
import barabadb/vector/engine as vengine
import barabadb/graph/engine as gengine

proc execSql(ctx: ExecutionContext, sql: string): ExecResult =
  let node = parse(sql)
  result = executeQuery(ctx, node)

suite "Schema persistence":
  test "CREATE TABLE survives flush + reopen":
    let dir = "/tmp/baradb_schema_persist_1"
    removeDir(dir)
    block:
      var db = newLSMTree(dir, 1024)  # small memtable → forces flush
      var ctx = newExecutionContext(db)
      let r = execSql(ctx, "CREATE TABLE users (id INT PRIMARY KEY, name TEXT NOT NULL)")
      check r.success
      check ctx.tables.hasKey("users")
      check ctx.tables["users"].columns.len == 2
      discard execSql(ctx, "INSERT INTO users (id, name) VALUES (1, 'Alice')")
      discard execSql(ctx, "INSERT INTO users (id, name) VALUES (2, 'Bob')")
      db.flush()
      # Schema key must be durable
      let (found, _) = db.get(tableSchemaKey("users"))
      check found
      db.close()

    # Reopen fresh context (simulates process restart)
    block:
      var db2 = newLSMTree(dir, 1024)
      var ctx2 = newExecutionContext(db2)
      check ctx2.tables.hasKey("users")
      check ctx2.tables["users"].columns.len == 2
      check ctx2.tables["users"].pkColumns.len == 1
      let sel = execSql(ctx2, "SELECT id, name FROM users ORDER BY id")
      check sel.success
      check sel.rows.len == 2
      db2.close()

  test "DROP TABLE removes schema and data":
    let dir = "/tmp/baradb_schema_persist_drop"
    removeDir(dir)
    var db = newLSMTree(dir)
    var ctx = newExecutionContext(db)
    check execSql(ctx, "CREATE TABLE t (id INT PRIMARY KEY)").success
    check execSql(ctx, "INSERT INTO t (id) VALUES (1)").success
    check execSql(ctx, "DROP TABLE t").success
    check not ctx.tables.hasKey("t")
    let (found, _) = db.get(tableSchemaKey("t"))
    check not found
    # Reopen — table must not reappear
    db.close()
    var db2 = newLSMTree(dir)
    var ctx2 = newExecutionContext(db2)
    check not ctx2.tables.hasKey("t")
    db2.close()

  test "ALTER TABLE ADD COLUMN is persisted":
    let dir = "/tmp/baradb_schema_persist_alter"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE items (id INT PRIMARY KEY)").success
      check execSql(ctx, "ALTER TABLE items ADD COLUMN label TEXT").success
      check ctx.tables["items"].columns.len == 2
      db.flush()
      db.close()
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      check ctx2.tables.hasKey("items")
      check ctx2.tables["items"].columns.len == 2
      var names: seq[string] = @[]
      for c in ctx2.tables["items"].columns:
        names.add(c.name)
      check "label" in names
      db2.close()

  test "Multiple tables all restored":
    let dir = "/tmp/baradb_schema_persist_multi"
    removeDir(dir)
    block:
      var db = newLSMTree(dir, 512)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE a (id INT PRIMARY KEY)").success
      check execSql(ctx, "CREATE TABLE b (id INT PRIMARY KEY, a_id INT)").success
      check execSql(ctx, "CREATE TABLE c (name TEXT)").success
      for i in 0..20:
        discard execSql(ctx, "INSERT INTO a (id) VALUES (" & $i & ")")
      db.flush()
      db.close()
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      check ctx2.tables.hasKey("a")
      check ctx2.tables.hasKey("b")
      check ctx2.tables.hasKey("c")
      let sel = execSql(ctx2, "SELECT id FROM a")
      check sel.success
      check sel.rows.len == 21
      db2.close()

  test "FTS index survives reopen":
    let dir = "/tmp/baradb_schema_persist_fts"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
      check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
      check execSql(ctx, "CREATE INDEX docs_fts ON docs (content) USING FTS").success
      check ctx.ftsIndexes.hasKey("docs.content")
      db.close()
    # Reopen fresh context (simulates process restart)
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      # Index must be rebuilt from the persisted schema key — today it is
      # silently missing, so FTS queries return empty results after reopen.
      check ctx2.ftsIndexes.hasKey("docs.content")
      if ctx2.ftsIndexes.hasKey("docs.content"):
        check ctx2.ftsIndexes["docs.content"].search("quick", limit = 10).len >= 1
      let r = execSql(ctx2, "SELECT id FROM docs WHERE content @@ 'quick'")
      check r.success
      check r.rows.len == 1
      # index keeps updating after reopen
      check execSql(ctx2, "INSERT INTO docs (id, content) VALUES (2, 'quick red fox')").success
      if ctx2.ftsIndexes.hasKey("docs.content"):
        check ctx2.ftsIndexes["docs.content"].search("red", limit = 10).len >= 1
      db2.close()
    removeDir(dir)

  test "HNSW vector index survives reopen":
    let dir = "/tmp/baradb_schema_persist_vec"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE vecs (id INTEGER PRIMARY KEY, embedding TEXT)").success
      check execSql(ctx, "INSERT INTO vecs (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')").success
      check execSql(ctx, "CREATE INDEX vecs_hnsw ON vecs (embedding) USING HNSW").success
      check ctx.vectorIndexes.hasKey("vecs.embedding")
      db.close()
    # Reopen fresh context (simulates process restart)
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      # Index must be rebuilt from the persisted schema key — today it is
      # silently missing, so vector searches return empty results after reopen.
      check ctx2.vectorIndexes.hasKey("vecs.embedding")
      if ctx2.vectorIndexes.hasKey("vecs.embedding"):
        check vengine.search(ctx2.vectorIndexes["vecs.embedding"],
                             @[1.0'f32, 0.0'f32, 0.0'f32], k = 5).len >= 1
      # index keeps updating after reopen
      check execSql(ctx2, "INSERT INTO vecs (id, embedding) VALUES (2, '[0.0, 1.0, 0.0]')").success
      if ctx2.vectorIndexes.hasKey("vecs.embedding"):
        check vengine.search(ctx2.vectorIndexes["vecs.embedding"],
                             @[0.0'f32, 1.0'f32, 0.0'f32], k = 5).len >= 1
      db2.close()
    removeDir(dir)

  test "Unnamed FTS index survives reopen":
    let dir = "/tmp/baradb_schema_persist_fts_noname"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
      check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
      # No index name — idxName defaults to colKey (docs.content) at execution
      check execSql(ctx, "CREATE INDEX ON docs (content) USING FTS").success
      check ctx.ftsIndexes.hasKey("docs.content")
      db.close()
    # Reopen fresh context (simulates process restart)
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      # Persisted DDL must be replayable — the dotted fallback name is not.
      check ctx2.ftsIndexes.hasKey("docs.content")
      if ctx2.ftsIndexes.hasKey("docs.content"):
        check ctx2.ftsIndexes["docs.content"].search("quick", limit = 10).len >= 1
      # Nameless persisted DDL must not confuse DROP INDEX name matching
      let d = execSql(ctx2, "DROP INDEX content")
      check d.success
      check "docs.content" notin ctx2.ftsIndexes
      let (found, _) = db2.get(SchemaFtsIndexPrefix & "docs.content")
      check not found
      db2.close()
    block:
      var db3 = newLSMTree(dir)
      var ctx3 = newExecutionContext(db3)
      check "docs.content" notin ctx3.ftsIndexes  # no ghost rebuild
      db3.close()
    removeDir(dir)

  test "Unnamed HNSW index survives reopen":
    let dir = "/tmp/baradb_schema_persist_vec_noname"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE vecs (id INTEGER PRIMARY KEY, embedding TEXT)").success
      check execSql(ctx, "INSERT INTO vecs (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')").success
      # No index name — idxName defaults to colKey (vecs.embedding)
      check execSql(ctx, "CREATE INDEX ON vecs (embedding) USING HNSW").success
      check ctx.vectorIndexes.hasKey("vecs.embedding")
      db.close()
    # Reopen fresh context (simulates process restart)
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      check ctx2.vectorIndexes.hasKey("vecs.embedding")
      if ctx2.vectorIndexes.hasKey("vecs.embedding"):
        check vengine.search(ctx2.vectorIndexes["vecs.embedding"],
                             @[1.0'f32, 0.0'f32, 0.0'f32], k = 5).len >= 1
      let d = execSql(ctx2, "DROP INDEX embedding")
      check d.success
      check "vecs.embedding" notin ctx2.vectorIndexes
      let (found, _) = db2.get(SchemaVecIndexPrefix & "vecs.embedding")
      check not found
      db2.close()
    block:
      var db3 = newLSMTree(dir)
      var ctx3 = newExecutionContext(db3)
      check "vecs.embedding" notin ctx3.vectorIndexes  # no ghost rebuild
      db3.close()
    removeDir(dir)

  test "Graph survives reopen":
    let dir = "/tmp/baradb_schema_persist_graph"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE GRAPH social").success
      check execSql(ctx, "INSERT INTO social_nodes (id, node_label) VALUES (1, 'person')").success
      check execSql(ctx, "INSERT INTO social_nodes (id, node_label) VALUES (2, 'person')").success
      check execSql(ctx, "INSERT INTO social_edges (source_id, dest_id, edge_label, weight) VALUES (1, 2, 'knows', 1.0)").success
      check "social" in ctx.graphs
      db.close()
    # Reopen fresh context (simulates process restart)
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      # Graph must be rebuilt from the backing tables — today it is
      # silently missing after reopen.
      check "social" in ctx2.graphs
      if "social" in ctx2.graphs:
        check gengine.nodeCount(ctx2.graphs["social"]) == 2
        check gengine.edgeCount(ctx2.graphs["social"]) == 1
      db2.close()
    removeDir(dir)

  test "DROP INDEX removes FTS index and its schema key (custom name)":
    let dir = "/tmp/baradb_schema_persist_dropfts"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
      check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
      check execSql(ctx, "CREATE INDEX docs_fts ON docs (content) USING FTS").success
      check ctx.ftsIndexes.hasKey("docs.content")
      # Drop by the custom index name — the in-memory key is table.col
      let d = execSql(ctx, "DROP INDEX docs_fts")
      check d.success
      check "docs.content" notin ctx.ftsIndexes
      let (found, _) = db.get(SchemaFtsIndexPrefix & "docs.content")
      check not found
      db.close()
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      check "docs.content" notin ctx2.ftsIndexes  # no ghost rebuild
      db2.close()
    removeDir(dir)

  test "DROP INDEX removes FTS index by column key":
    let dir = "/tmp/baradb_schema_persist_dropfts_col"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
      check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
      # No custom name — idxName defaults to colKey (docs.content)
      check execSql(ctx, "CREATE INDEX ON docs (content) USING FTS").success
      check ctx.ftsIndexes.hasKey("docs.content")
      # Drop by column name — matches endsWith(".content")
      let d = execSql(ctx, "DROP INDEX content")
      check d.success
      check "docs.content" notin ctx.ftsIndexes
      let (found, _) = db.get(SchemaFtsIndexPrefix & "docs.content")
      check not found
      db.close()
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      check "docs.content" notin ctx2.ftsIndexes  # no ghost rebuild
      db2.close()
    removeDir(dir)

  test "DROP INDEX removes HNSW index and its schema key":
    let dir = "/tmp/baradb_schema_persist_drophnsw"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE vecs (id INTEGER PRIMARY KEY, embedding TEXT)").success
      check execSql(ctx, "INSERT INTO vecs (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')").success
      check execSql(ctx, "CREATE INDEX vecs_hnsw ON vecs (embedding) USING HNSW").success
      check ctx.vectorIndexes.hasKey("vecs.embedding")
      let d = execSql(ctx, "DROP INDEX vecs_hnsw")
      check d.success
      check "vecs.embedding" notin ctx.vectorIndexes
      let (found, _) = db.get(SchemaVecIndexPrefix & "vecs.embedding")
      check not found
      db.close()
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      check "vecs.embedding" notin ctx2.vectorIndexes  # no ghost rebuild
      db2.close()
    removeDir(dir)

  test "DROP TABLE removes engine indexes for that table":
    let dir = "/tmp/baradb_schema_persist_droptbl"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
      check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
      check execSql(ctx, "CREATE INDEX docs_fts ON docs (content) USING FTS").success
      check execSql(ctx, "CREATE TABLE vecs (id INTEGER PRIMARY KEY, embedding TEXT)").success
      check execSql(ctx, "INSERT INTO vecs (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')").success
      check execSql(ctx, "CREATE INDEX vecs_hnsw ON vecs (embedding) USING HNSW").success
      check ctx.ftsIndexes.hasKey("docs.content")
      check ctx.vectorIndexes.hasKey("vecs.embedding")
      check execSql(ctx, "DROP TABLE docs").success
      check "docs.content" notin ctx.ftsIndexes
      check execSql(ctx, "DROP TABLE vecs").success
      check "vecs.embedding" notin ctx.vectorIndexes
      let (ftsFound, _) = db.get(SchemaFtsIndexPrefix & "docs.content")
      check not ftsFound
      let (vecFound, _) = db.get(SchemaVecIndexPrefix & "vecs.embedding")
      check not vecFound
      db.close()
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      check "docs.content" notin ctx2.ftsIndexes    # no ghost rebuild
      check "vecs.embedding" notin ctx2.vectorIndexes
      db2.close()
    removeDir(dir)

  test "B-tree index survives reopen":
    let dir = "/tmp/baradb_schema_persist_btree"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)").success
      check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)").success
      check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25)").success
      check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (3, 'Carol', 40)").success
      check execSql(ctx, "CREATE INDEX users_age ON users (age)").success
      check "users.age" in ctx.btrees
      db.close()
    # Reopen fresh context (simulates process restart)
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      check "users.age" in ctx2.btrees
      let r = execSql(ctx2, "SELECT id FROM users WHERE age = 25")
      check r.success
      check r.rows.len == 1
      # index keeps updating after reopen
      check execSql(ctx2, "INSERT INTO users (id, name, age) VALUES (4, 'Dan', 55)").success
      if "users.age" in ctx2.btrees:
        check ctx2.btrees["users.age"].get("55").len >= 1
      db2.close()
    removeDir(dir)

  test "Unnamed B-tree index survives reopen":
    let dir = "/tmp/baradb_schema_persist_btree_noname"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)").success
      check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)").success
      # No index name — idxName defaults to colKey (users.age) at execution
      check execSql(ctx, "CREATE INDEX ON users (age)").success
      check "users.age" in ctx.btrees
      db.close()
    # Reopen fresh context (simulates process restart)
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      # Persisted DDL must be replayable — the dotted fallback name is not.
      check "users.age" in ctx2.btrees
      let r = execSql(ctx2, "SELECT id FROM users WHERE age = 30")
      check r.success
      check r.rows.len == 1
      db2.close()
    removeDir(dir)

  test "DROP INDEX removes B-tree index and its schema key":
    let dir = "/tmp/baradb_schema_persist_dropbtree"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)").success
      check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)").success
      check execSql(ctx, "CREATE INDEX users_age ON users (age)").success
      check "users.age" in ctx.btrees
      let (foundBefore, _) = db.get(SchemaBtreeIndexPrefix & "users.age")
      check foundBefore
      let d = execSql(ctx, "DROP INDEX users_age")
      check d.success
      check "users.age" notin ctx.btrees
      let (found, _) = db.get(SchemaBtreeIndexPrefix & "users.age")
      check not found
      db.close()
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      check "users.age" notin ctx2.btrees  # no ghost rebuild
      db2.close()
    removeDir(dir)

  test "UNIQUE B-tree index survives reopen and still enforces":
    let dir = "/tmp/baradb_schema_persist_unique"
    removeDir(dir)
    block:
      var db = newLSMTree(dir)
      var ctx = newExecutionContext(db)
      check execSql(ctx, "CREATE TABLE accts (id INTEGER PRIMARY KEY, email TEXT)").success
      check execSql(ctx, "CREATE UNIQUE INDEX accts_email ON accts (email)").success
      check execSql(ctx, "INSERT INTO accts (id, email) VALUES (1, 'a@b.c')").success
      check "accts.email" in ctx.btrees
      db.close()
    # Reopen fresh context (simulates process restart)
    block:
      var db2 = newLSMTree(dir)
      var ctx2 = newExecutionContext(db2)
      # UNIQUE flag must survive restart via the persisted DDL.
      check "accts.email" in ctx2.btrees
      let dup = execSql(ctx2, "INSERT INTO accts (id, email) VALUES (2, 'a@b.c')")
      check not dup.success
      let ok = execSql(ctx2, "INSERT INTO accts (id, email) VALUES (2, 'x@y.z')")
      check ok.success
      db2.close()
    removeDir(dir)

  test "Stable schema key format":
    check tableSchemaKey("users") == "_schema:tables:users"
    check serializeTableDdl(TableDef(
      name: "t",
      columns: @[ColumnDef(name: "id", colType: "INT", isPk: true)],
      pkColumns: @["id"],
    )).contains("PRIMARY KEY")
