## Property-Based Tests — evalExprValue + B-Tree invariants
import std/unittest
import std/tables
import std/random
import std/os
import std/monotimes
import std/math
import std/sets

import barabadb/core/types
import barabadb/storage/lsm
import barabadb/storage/btree
import barabadb/query/ir as qir
import barabadb/query/executor as qexec
from barabadb/query/executor import Row

suite "Property-Based — evalExprValue Invariants":
  setup:
    var testDir = getTempDir() / "baradb_prop_test_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks
    createDir(testDir)
    var db = newLSMTree(testDir)
    var ctx {.used.} = qexec.newExecutionContext(db)

  teardown:
    removeDir(testDir)

  proc randIntLit(rng: var Rand, minVal: int = -1000, maxVal: int = 1000): IRExpr =
    result = IRExpr(kind: irekLiteral, valueKind: vkInt64)
    result.literal = IRLiteral(kind: vkInt64, int64Val: int64(rng.rand(minVal..maxVal)))

  proc randFloatLit(rng: var Rand, minVal: float = -1000.0, maxVal: float = 1000.0): IRExpr =
    result = IRExpr(kind: irekLiteral, valueKind: vkFloat64)
    result.literal = IRLiteral(kind: vkFloat64, float64Val: minVal + rng.rand(maxVal - minVal))

  proc randStrLit(rng: var Rand, minLen: int = 0, maxLen: int = 10): IRExpr =
    result = IRExpr(kind: irekLiteral, valueKind: vkString)
    let len = rng.rand(minLen..maxLen)
    var s = ""
    for i in 0..<len:
      s.add(char(rng.rand(ord('a')..ord('z'))))
    result.literal = IRLiteral(kind: vkString, strVal: s)

  proc randBinaryExpr(rng: var Rand, left, right: IRExpr, op: IROperator): IRExpr =
    result = IRExpr(kind: irekBinary)
    result.binOp = op
    result.binLeft = left
    result.binRight = right
    if left.valueKind == vkInt64 and right.valueKind == vkInt64 and op != irDiv:
      result.valueKind = vkInt64
    else:
      result.valueKind = vkFloat64

  proc randUnaryExpr(rng: var Rand, expr: IRExpr, op: IROperator): IRExpr =
    result = IRExpr(kind: irekUnary)
    result.unOp = op
    result.unExpr = expr
    result.valueKind = expr.valueKind

  proc nullLit(): IRExpr =
    result = IRExpr(kind: irekLiteral, valueKind: vkNull)
    result.literal = IRLiteral(kind: vkNull)

  proc intLit(val: int64): IRExpr =
    result = IRExpr(kind: irekLiteral, valueKind: vkInt64)
    result.literal = IRLiteral(kind: vkInt64, int64Val: val)

  proc floatLit(val: float64): IRExpr =
    result = IRExpr(kind: irekLiteral, valueKind: vkFloat64)
    result.literal = IRLiteral(kind: vkFloat64, float64Val: val)

  proc makeBinary(left, right: IRExpr, op: IROperator, vk: ValueKind = vkInt64): IRExpr =
    result = IRExpr(kind: irekBinary)
    result.binOp = op
    result.binLeft = left
    result.binRight = right
    result.valueKind = vk

  proc makeUnary(expr: IRExpr, op: IROperator, vk: ValueKind = vkInt64): IRExpr =
    result = IRExpr(kind: irekUnary)
    result.unOp = op
    result.unExpr = expr
    result.valueKind = vk

  # ──────────────────────────────────────────────────
  # Literal tests
  # ──────────────────────────────────────────────────
  test "Literal eval returns correct ValueKind (INT)":
    var rng = initRand(42)
    for i in 0..<100:
      let lit = randIntLit(rng)
      let v = qexec.evalExpr(lit, Row(), nil)
      check v.kind == vkInt64

  test "Literal eval returns correct ValueKind (FLOAT)":
    var rng = initRand(43)
    for i in 0..<100:
      let lit = randFloatLit(rng)
      let v = qexec.evalExpr(lit, Row(), nil)
      check v.kind == vkFloat64

  test "Literal eval returns correct ValueKind (STRING)":
    var rng = initRand(49)
    for i in 0..<100:
      let lit = randStrLit(rng)
      let v = qexec.evalExpr(lit, Row(), nil)
      check v.kind == vkString

  test "Literal eval returns correct ValueKind (BOOL)":
    for b in [true, false]:
      var lit = IRExpr(kind: irekLiteral, valueKind: vkBool)
      lit.literal = IRLiteral(kind: vkBool, boolVal: b)
      let v = qexec.evalExpr(lit, Row(), nil)
      check v.kind == vkBool
      check v.boolVal == b

  test "Literal eval returns correct ValueKind (NULL)":
    let lit = nullLit()
    let v = qexec.evalExpr(lit, Row(), nil)
    check v.kind == vkNull

  # ──────────────────────────────────────────────────
  # Commutativity
  # ──────────────────────────────────────────────────
  test "INT addition is commutative":
    var rng = initRand(44)
    for i in 0..<100:
      let a = randIntLit(rng)
      let b = randIntLit(rng)
      let sum1 = qexec.evalExpr(randBinaryExpr(rng, a, b, irAdd), Row(), nil)
      let sum2 = qexec.evalExpr(randBinaryExpr(rng, b, a, irAdd), Row(), nil)
      if sum1.kind == vkInt64 and sum2.kind == vkInt64:
        check sum1.int64Val == sum2.int64Val

  test "FLOAT addition is commutative":
    var rng = initRand(45)
    for i in 0..<100:
      let a = randFloatLit(rng)
      let b = randFloatLit(rng)
      let sum1 = qexec.evalExpr(randBinaryExpr(rng, a, b, irAdd), Row(), nil)
      let sum2 = qexec.evalExpr(randBinaryExpr(rng, b, a, irAdd), Row(), nil)
      if sum1.kind == vkFloat64 and sum2.kind == vkFloat64:
        check abs(sum1.float64Val - sum2.float64Val) < 1e-9

  test "INT multiplication is commutative":
    var rng = initRand(50)
    for i in 0..<100:
      let a = randIntLit(rng)
      let b = randIntLit(rng)
      let prod1 = qexec.evalExpr(randBinaryExpr(rng, a, b, irMul), initTable[string, Value](), nil)
      let prod2 = qexec.evalExpr(randBinaryExpr(rng, b, a, irMul), initTable[string, Value](), nil)
      if prod1.kind == vkInt64 and prod2.kind == vkInt64:
        check prod1.int64Val == prod2.int64Val

  test "FLOAT multiplication is commutative":
    var rng = initRand(51)
    for i in 0..<100:
      let a = randFloatLit(rng)
      let b = randFloatLit(rng)
      let prod1 = qexec.evalExpr(randBinaryExpr(rng, a, b, irMul), initTable[string, Value](), nil)
      let prod2 = qexec.evalExpr(randBinaryExpr(rng, b, a, irMul), initTable[string, Value](), nil)
      if prod1.kind == vkFloat64 and prod2.kind == vkFloat64:
        check abs(prod1.float64Val - prod2.float64Val) < 1e-9

  # ──────────────────────────────────────────────────
  # Associativity
  # ──────────────────────────────────────────────────
  test "INT addition is associative":
    var rng = initRand(52)
    for i in 0..<100:
      let a = randIntLit(rng)
      let b = randIntLit(rng)
      let c = randIntLit(rng)
      let abPlusC = makeBinary(makeBinary(a, b, irAdd, vkInt64), c, irAdd, vkInt64)
      let aPlusBC = makeBinary(a, makeBinary(b, c, irAdd, vkInt64), irAdd, vkInt64)
      let v1 = qexec.evalExpr(abPlusC, initTable[string, Value](), nil)
      let v2 = qexec.evalExpr(aPlusBC, initTable[string, Value](), nil)
      if v1.kind == vkInt64 and v2.kind == vkInt64:
        check v1.int64Val == v2.int64Val

  test "FLOAT addition is associative":
    var rng = initRand(53)
    for i in 0..<100:
      let a = randFloatLit(rng)
      let b = randFloatLit(rng)
      let c = randFloatLit(rng)
      let abPlusC = makeBinary(makeBinary(a, b, irAdd, vkFloat64), c, irAdd, vkFloat64)
      let aPlusBC = makeBinary(a, makeBinary(b, c, irAdd, vkFloat64), irAdd, vkFloat64)
      let v1 = qexec.evalExpr(abPlusC, initTable[string, Value](), nil)
      let v2 = qexec.evalExpr(aPlusBC, initTable[string, Value](), nil)
      if v1.kind == vkFloat64 and v2.kind == vkFloat64:
        check abs(v1.float64Val - v2.float64Val) < 1e-9

  test "INT multiplication is associative":
    var rng = initRand(54)
    for i in 0..<100:
      let a = randIntLit(rng)
      let b = randIntLit(rng)
      let c = randIntLit(rng)
      let abMulC = makeBinary(makeBinary(a, b, irMul, vkInt64), c, irMul, vkInt64)
      let aMulBC = makeBinary(a, makeBinary(b, c, irMul, vkInt64), irMul, vkInt64)
      let v1 = qexec.evalExpr(abMulC, initTable[string, Value](), nil)
      let v2 = qexec.evalExpr(aMulBC, initTable[string, Value](), nil)
      if v1.kind == vkInt64 and v2.kind == vkInt64:
        check v1.int64Val == v2.int64Val

  test "STRING concatenation is associative":
    var rng = initRand(55)
    for i in 0..<100:
      let a = randStrLit(rng, 0, 5)
      let b = randStrLit(rng, 0, 5)
      let c = randStrLit(rng, 0, 5)
      let abConcatC = makeBinary(makeBinary(a, b, irAdd, vkString), c, irAdd, vkString)
      let aConcatBC = makeBinary(a, makeBinary(b, c, irAdd, vkString), irAdd, vkString)
      let v1 = qexec.evalExpr(abConcatC, initTable[string, Value](), nil)
      let v2 = qexec.evalExpr(aConcatBC, initTable[string, Value](), nil)
      if v1.kind == vkString and v2.kind == vkString:
        check v1.strVal == v2.strVal

  # ──────────────────────────────────────────────────
  # Distributivity
  # ──────────────────────────────────────────────────
  test "INT distributivity: a*(b+c) == a*b + a*c":
    var rng = initRand(56)
    for i in 0..<100:
      let a = randIntLit(rng)
      let b = randIntLit(rng)
      let c = randIntLit(rng)
      let left = makeBinary(a, makeBinary(b, c, irAdd, vkInt64), irMul, vkInt64)
      let ab = makeBinary(a, b, irMul, vkInt64)
      let ac = makeBinary(a, c, irMul, vkInt64)
      let right = makeBinary(ab, ac, irAdd, vkInt64)
      let v1 = qexec.evalExpr(left, initTable[string, Value](), nil)
      let v2 = qexec.evalExpr(right, initTable[string, Value](), nil)
      if v1.kind == vkInt64 and v2.kind == vkInt64:
        check v1.int64Val == v2.int64Val

  # ──────────────────────────────────────────────────
  # Identity
  # ──────────────────────────────────────────────────
  test "INT multiplication by 1 is identity":
    var rng = initRand(46)
    for i in 0..<100:
      let a = randIntLit(rng)
      let one = intLit(1)
      let prod = qexec.evalExpr(randBinaryExpr(rng, a, one, irMul), initTable[string, Value](), nil)
      if prod.kind == vkInt64:
        check prod.int64Val == a.literal.int64Val

  test "INT addition with 0 is identity":
    var rng = initRand(57)
    for i in 0..<100:
      let a = randIntLit(rng)
      let zero = intLit(0)
      let sum = qexec.evalExpr(randBinaryExpr(rng, a, zero, irAdd), initTable[string, Value](), nil)
      if sum.kind == vkInt64:
        check sum.int64Val == a.literal.int64Val

  test "FLOAT addition with 0.0 is identity":
    var rng = initRand(58)
    for i in 0..<100:
      let a = randFloatLit(rng)
      let zero = floatLit(0.0)
      let expr = makeBinary(a, zero, irAdd, vkFloat64)
      let sum = qexec.evalExpr(expr, initTable[string, Value](), nil)
      if sum.kind == vkFloat64:
        check abs(sum.float64Val - a.literal.float64Val) < 1e-9

  test "INT subtraction: a - 0 == a":
    var rng = initRand(59)
    for i in 0..<100:
      let a = randIntLit(rng)
      let zero = intLit(0)
      let expr = makeBinary(a, zero, irSub, vkInt64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      if v.kind == vkInt64:
        check v.int64Val == a.literal.int64Val

  test "INT subtraction: a - a == 0":
    var rng = initRand(60)
    for i in 0..<100:
      let a = randIntLit(rng)
      let expr = randBinaryExpr(rng, a, a, irSub)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      if v.kind == vkInt64:
        check v.int64Val == 0

  test "FLOAT subtraction: a - a == 0":
    var rng = initRand(61)
    for i in 0..<100:
      let a = randFloatLit(rng)
      let expr = makeBinary(a, a, irSub, vkFloat64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      if v.kind == vkFloat64:
        check abs(v.float64Val) < 1e-9

  test "FLOAT multiplication by 1.0 is identity":
    var rng = initRand(62)
    for i in 0..<100:
      let a = randFloatLit(rng)
      let one = floatLit(1.0)
      let expr = makeBinary(a, one, irMul, vkFloat64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      if v.kind == vkFloat64:
        check abs(v.float64Val - a.literal.float64Val) < 1e-9

  test "STRING concatenation with empty string is identity":
    var rng = initRand(63)
    for i in 0..<100:
      let a = randStrLit(rng, 0, 10)
      let empty = randStrLit(rng, 0, 0)
      let v1 = qexec.evalExpr(makeBinary(a, empty, irAdd, vkString), initTable[string, Value](), nil)
      let v2 = qexec.evalExpr(makeBinary(empty, a, irAdd, vkString), initTable[string, Value](), nil)
      if v1.kind == vkString: check v1.strVal == a.literal.strVal
      if v2.kind == vkString: check v2.strVal == a.literal.strVal

  # ──────────────────────────────────────────────────
  # Negation
  # ──────────────────────────────────────────────────
  test "Double negation of INT is identity":
    var rng = initRand(47)
    for i in 0..<100:
      let a = randIntLit(rng)
      let neg = randUnaryExpr(rng, a, irNeg)
      let negNeg = randUnaryExpr(rng, neg, irNeg)
      let v = qexec.evalExpr(negNeg, initTable[string, Value](), nil)
      if v.kind == vkInt64:
        check v.int64Val == a.literal.int64Val

  test "Double negation of FLOAT is identity":
    var rng = initRand(48)
    for i in 0..<100:
      let a = randFloatLit(rng)
      let neg = randUnaryExpr(rng, a, irNeg)
      let negNeg = randUnaryExpr(rng, neg, irNeg)
      let v = qexec.evalExpr(negNeg, initTable[string, Value](), nil)
      if v.kind == vkFloat64:
        check abs(v.float64Val - a.literal.float64Val) < 1e-9

  test "Negated zero is zero (INT)":
    let zero = intLit(0)
    let negZero = makeUnary(zero, irNeg, vkInt64)
    let v = qexec.evalExpr(negZero, initTable[string, Value](), nil)
    if v.kind == vkInt64:
      check v.int64Val == 0

  test "Negated zero is zero (FLOAT)":
    let zero = floatLit(0.0)
    let negZero = makeUnary(zero, irNeg, vkFloat64)
    let v = qexec.evalExpr(negZero, initTable[string, Value](), nil)
    if v.kind == vkFloat64:
      check v.float64Val == 0.0

  # ──────────────────────────────────────────────────
  # Division & Mod
  # ──────────────────────────────────────────────────
  test "Division: (a * b) / b == a when b != 0 (INT→FLOAT)":
    var rng = initRand(64)
    for i in 0..<100:
      let a = int64(rng.rand(-100..100))
      let b = int64(rng.rand(-100..100))
      if b == 0: continue
      let av = intLit(a)
      let bv = intLit(b)
      let mul = makeBinary(av, bv, irMul, vkInt64)
      let divE = makeBinary(mul, bv, irDiv, vkFloat64)
      let v = qexec.evalExpr(divE, initTable[string, Value](), nil)
      if v.kind == vkFloat64:
        check abs(v.float64Val - float64(a)) < 1e-6

  test "Mod with positive operands returns valid remainder":
    var rng = initRand(65)
    for i in 0..<100:
      let a = int64(rng.rand(0..100))
      let b = int64(rng.rand(1..100))
      let av = intLit(a)
      let bv = intLit(b)
      let modE = makeBinary(av, bv, irMod, vkInt64)
      let v = qexec.evalExpr(modE, initTable[string, Value](), nil)
      if v.kind == vkInt64:
        check v.int64Val >= 0
        check v.int64Val < b

  # ──────────────────────────────────────────────────
  # Division by zero → NULL
  # ──────────────────────────────────────────────────
  test "INT division by zero returns NULL":
    let a = intLit(42)
    let zero = intLit(0)
    let divExpr = makeBinary(a, zero, irDiv, vkFloat64)
    let v = qexec.evalExpr(divExpr, initTable[string, Value](), nil)
    check v.kind == vkNull

  test "FLOAT division by zero returns NULL":
    let a = floatLit(42.0)
    let zero = floatLit(0.0)
    let divExpr = makeBinary(a, zero, irDiv, vkFloat64)
    let v = qexec.evalExpr(divExpr, initTable[string, Value](), nil)
    check v.kind == vkNull

  test "INT modulo zero returns NULL":
    let a = intLit(42)
    let zero = intLit(0)
    let modExpr = makeBinary(a, zero, irMod, vkInt64)
    let v = qexec.evalExpr(modExpr, initTable[string, Value](), nil)
    check v.kind == vkNull

  # ──────────────────────────────────────────────────
  # POW
  # ──────────────────────────────────────────────────
  test "POW(a, 0) == 1.0":
    var rng = initRand(66)
    for i in 0..<50:
      let a = randIntLit(rng)
      let zero = intLit(0)
      let powExpr = makeBinary(a, zero, irPow, vkFloat64)
      let v = qexec.evalExpr(powExpr, initTable[string, Value](), nil)
      if v.kind == vkFloat64:
        check abs(v.float64Val - 1.0) < 1e-9

  test "POW(a, 1) == a":
    var rng = initRand(67)
    for i in 0..<50:
      let a = randIntLit(rng)
      let one = intLit(1)
      let powExpr = makeBinary(a, one, irPow, vkFloat64)
      let v = qexec.evalExpr(powExpr, initTable[string, Value](), nil)
      if v.kind == vkFloat64:
        check abs(v.float64Val - float64(a.literal.int64Val)) < 1e-9

  test "POW(a, 2) == a*a":
    var rng = initRand(68)
    for i in 0..<50:
      let a = randIntLit(rng)
      let two = intLit(2)
      let powExpr = makeBinary(a, two, irPow, vkFloat64)
      let mulExpr = makeBinary(a, a, irMul, vkInt64)
      let v1 = qexec.evalExpr(powExpr, initTable[string, Value](), nil)
      let v2 = qexec.evalExpr(mulExpr, initTable[string, Value](), nil)
      if v1.kind == vkFloat64 and v2.kind == vkInt64:
        check abs(v1.float64Val - float64(v2.int64Val)) < 1e-9

  # ──────────────────────────────────────────────────
  # NULL propagation
  # ──────────────────────────────────────────────────
  test "NULL literal propagates through arithmetic":
    let nullLit = IRExpr(kind: irekLiteral, valueKind: vkNull)
    nullLit.literal = IRLiteral(kind: vkNull)
    let intLit = IRExpr(kind: irekLiteral, valueKind: vkInt64)
    intLit.literal = IRLiteral(kind: vkInt64, int64Val: 5)
    for op in [irAdd, irSub, irMul, irDiv]:
      let expr = IRExpr(kind: irekBinary)
      expr.binOp = op
      expr.binLeft = nullLit
      expr.binRight = intLit
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      check v.kind == vkNull

  test "NULL propagates through mod":
    let n = nullLit()
    let a = intLit(5)
    for pair in [(n, a), (a, n)]:
      let (left, right) = pair
      let expr = makeBinary(left, right, irMod, vkInt64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      check v.kind == vkNull

  test "NULL propagates through pow":
    let n = nullLit()
    let a = intLit(5)
    for pair in [(n, a), (a, n)]:
      let (left, right) = pair
      let expr = makeBinary(left, right, irPow, vkFloat64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      check v.kind == vkNull

  test "NULL propagates through negation":
    let n = nullLit()
    let neg = makeUnary(n, irNeg, vkInt64)
    let v = qexec.evalExpr(neg, initTable[string, Value](), nil)
    check v.kind == vkNull

  # ──────────────────────────────────────────────────
  # Type coercion / mixed-type arithmetic
  # ──────────────────────────────────────────────────
  test "INT + FLOAT → FLOAT":
    var rng = initRand(69)
    for i in 0..<50:
      let a = randIntLit(rng)
      let b = randFloatLit(rng)
      let expr = makeBinary(a, b, irAdd, vkFloat64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      check v.kind == vkFloat64

  test "FLOAT + INT → FLOAT":
    var rng = initRand(70)
    for i in 0..<50:
      let a = randFloatLit(rng)
      let b = randIntLit(rng)
      let expr = makeBinary(a, b, irAdd, vkFloat64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      check v.kind == vkFloat64

  test "INT / INT → FLOAT":
    var rng = initRand(71)
    for i in 0..<50:
      let a = randIntLit(rng)
      var b = rng.rand(1..100)  # non-zero
      let bv = intLit(int64(b))
      let expr = makeBinary(a, bv, irDiv, vkFloat64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      check v.kind == vkFloat64

  test "INT - FLOAT → FLOAT":
    var rng = initRand(72)
    for i in 0..<50:
      let a = randIntLit(rng)
      let b = randFloatLit(rng)
      let expr = makeBinary(a, b, irSub, vkFloat64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      check v.kind == vkFloat64

  test "INT * FLOAT → FLOAT":
    var rng = initRand(73)
    for i in 0..<50:
      let a = randIntLit(rng)
      let b = randFloatLit(rng)
      let expr = makeBinary(a, b, irMul, vkFloat64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      check v.kind == vkFloat64

  test "FLOAT + FLOAT → FLOAT":
    var rng = initRand(74)
    for i in 0..<50:
      let a = randFloatLit(rng)
      let b = randFloatLit(rng)
      let expr = makeBinary(a, b, irAdd, vkFloat64)
      let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
      check v.kind == vkFloat64

  test "INT + INT → INT (non-div ops)":
    var rng = initRand(75)
    for i in 0..<50:
      let a = randIntLit(rng)
      let b = randIntLit(rng)
      for op in [irAdd, irSub, irMul]:
        let expr = makeBinary(a, b, op, vkInt64)
        let v = qexec.evalExpr(expr, initTable[string, Value](), nil)
        check v.kind == vkInt64

  # ──────────────────────────────────────────────────
  # Comparison evals (via evalExpr → string)
  # ──────────────────────────────────────────────────
  test "eq comparison: a == a is true":
    var rng = initRand(76)
    for i in 0..<50:
      let a = randIntLit(rng)
      let expr = makeBinary(a, a, irEq, vkBool)
      let s = evalExpr(expr, initTable[string, Value](), nil)
      check s == "true"

  test "neq comparison: a != a is false":
    var rng = initRand(77)
    for i in 0..<50:
      let a = randIntLit(rng)
      let expr = makeBinary(a, a, irNeq, vkBool)
      let s = evalExpr(expr, initTable[string, Value](), nil)
      check s == "false"

  test "lt comparison: a < a is false":
    let a = intLit(5)
    let expr = makeBinary(a, a, irLt, vkBool)
    let s = evalExpr(expr, initTable[string, Value](), nil)
    check s == "false"

  test "lte comparison: a <= a is true":
    let a = intLit(5)
    let expr = makeBinary(a, a, irLte, vkBool)
    let s = evalExpr(expr, initTable[string, Value](), nil)
    check s == "true"

  test "gt comparison: a > a is false":
    let a = intLit(5)
    let expr = makeBinary(a, a, irGt, vkBool)
    let s = evalExpr(expr, initTable[string, Value](), nil)
    check s == "false"

  test "gte comparison: a >= a is true":
    let a = intLit(5)
    let expr = makeBinary(a, a, irGte, vkBool)
    let s = evalExpr(expr, initTable[string, Value](), nil)
    check s == "true"

  test "lt comparison: a < b is true when a < b":
    var rng = initRand(78)
    for i in 0..<50:
      let x = int64(rng.rand(0..50))
      let y = int64(rng.rand(51..100))
      let a = intLit(x)
      let b = intLit(y)
      let expr = makeBinary(a, b, irLt, vkBool)
      let s = evalExpr(expr, initTable[string, Value](), nil)
      check s == "true"

  # ──────────────────────────────────────────────────
  # AND / OR logical operations
  # ──────────────────────────────────────────────────
  test "AND: true AND true = true":
    var ta = IRExpr(kind: irekLiteral, valueKind: vkBool)
    ta.literal = IRLiteral(kind: vkBool, boolVal: true)
    let expr = makeBinary(ta, ta, irAnd, vkBool)
    let s = evalExpr(expr, initTable[string, Value](), nil)
    check s == "true"

  test "AND: true AND false = false":
    var ta = IRExpr(kind: irekLiteral, valueKind: vkBool)
    ta.literal = IRLiteral(kind: vkBool, boolVal: true)
    var fa = IRExpr(kind: irekLiteral, valueKind: vkBool)
    fa.literal = IRLiteral(kind: vkBool, boolVal: false)
    let expr = makeBinary(ta, fa, irAnd, vkBool)
    let s = evalExpr(expr, initTable[string, Value](), nil)
    check s == "false"

  test "OR: false OR true = true":
    var ta = IRExpr(kind: irekLiteral, valueKind: vkBool)
    ta.literal = IRLiteral(kind: vkBool, boolVal: true)
    var fa = IRExpr(kind: irekLiteral, valueKind: vkBool)
    fa.literal = IRLiteral(kind: vkBool, boolVal: false)
    let expr = makeBinary(fa, ta, irOr, vkBool)
    let s = evalExpr(expr, initTable[string, Value](), nil)
    check s == "true"

  test "OR: false OR false = false":
    var fa = IRExpr(kind: irekLiteral, valueKind: vkBool)
    fa.literal = IRLiteral(kind: vkBool, boolVal: false)
    let expr = makeBinary(fa, fa, irOr, vkBool)
    let s = evalExpr(expr, initTable[string, Value](), nil)
    check s == "false"

  # ──────────────────────────────────────────────────
  # Random complex nested expressions
  # ──────────────────────────────────────────────────
  test "Complex nested expression does not crash":
    var rng = initRand(79)
    for i in 0..<200:
      let a = randIntLit(rng)
      let b = randIntLit(rng)
      let c = randIntLit(rng)
      let d = randIntLit(rng)
      let t1 = makeBinary(a, b, irAdd, vkInt64)
      let t2 = makeBinary(c, d, irMul, vkInt64)
      let t3 = makeBinary(t1, t2, irSub, vkInt64)
      let t4 = makeUnary(t3, irNeg, vkInt64)
      let t5 = makeBinary(t4, intLit(1), irAdd, vkInt64)
      discard qexec.evalExpr(t5, initTable[string, Value](), nil)
    check true

  test "Random binary tree depth 5 does not crash":
    var rng = initRand(80)
    let ops = [irAdd, irSub, irMul, irDiv, irMod, irPow]
    for i in 0..<200:
      var nodes: seq[IRExpr] = @[]
      for j in 0..<16:
        if rng.rand(0..1) == 0:
          nodes.add(randIntLit(rng))
        else:
          nodes.add(randFloatLit(rng))
      while nodes.len > 1:
        let opIdx = rng.rand(0..ops.len-1)
        let left = nodes.pop()
        let right = nodes.pop()
        let vk = if left.valueKind == vkInt64 and right.valueKind == vkInt64 and ops[opIdx] != irDiv: vkInt64
                 else: vkFloat64
        nodes.insert(makeBinary(left, right, ops[opIdx], vk), 0)
      if nodes.len == 1:
        discard qexec.evalExpr(nodes[0], initTable[string, Value](), nil)
    check true

  test "Nil expr evaluates to NULL":
    let v = qexec.evalExpr(nil, initTable[string, Value](), nil)
    check v.kind == vkNull
    let s = qexec.evalExpr(nil, initTable[string, Value](), nil)
    check s.kind == vkNull

# ═══════════════════════════════════════════════════
# B-Tree Property-Based Invariants
# ═══════════════════════════════════════════════════
suite "Property-Based — B-Tree Invariants":

  proc randKey(rng: var Rand, minVal: int = 0, maxVal: int = 10000): int =
    rng.rand(minVal..maxVal)

  test "B-Tree size equals number of unique keys after random inserts":
    var rng = initRand(1000)
    var btree = newBTreeIndex[int, string](order = 8)
    var uniqueKeys = initTable[int, bool]()
    for i in 0..<500:
      let k = randKey(rng)
      btree.insert(k, "v" & $k)
      uniqueKeys[k] = true
    check btree.len == uniqueKeys.len

  test "B-Tree get returns all values for inserted key":
    var rng = initRand(1001)
    var btree = newBTreeIndex[int, string](order = 8)
    var expected = initTable[int, seq[string]]()
    for i in 0..<200:
      let k = randKey(rng, 0, 50)
      let v = "val_" & $i
      btree.insert(k, v)
      if k notin expected:
        expected[k] = @[]
      expected[k].add(v)
    for k, vals in expected:
      let got = btree.get(k)
      check got == vals

  test "B-Tree scan returns keys in ascending order":
    var rng = initRand(1002)
    var btree = newBTreeIndex[int, string](order = 8)
    for i in 0..<300:
      btree.insert(randKey(rng, 0, 1000), "x")
    let result = btree.scan(0, 1000)
    for i in 1..<result.len:
      check result[i-1][0] <= result[i][0]

  test "B-Tree scan range is inclusive and correct":
    var rng = initRand(1003)
    var btree = newBTreeIndex[int, string](order = 8)
    var inserted = initTable[int, bool]()
    for i in 0..<400:
      let k = randKey(rng, 0, 200)
      btree.insert(k, "v")
      inserted[k] = true
    let scanned = btree.scan(50, 100)
    for (k, _) in scanned:
      check k >= 50
      check k <= 100
      check inserted[k]

  test "B-Tree contains after insert":
    var rng = initRand(1004)
    var btree = newBTreeIndex[int, string](order = 8)
    var keys: seq[int] = @[]
    for i in 0..<100:
      let k = randKey(rng)
      btree.insert(k, "v")
      keys.add(k)
    for k in keys:
      check btree.contains(k)

  test "B-Tree remove decreases size":
    var rng = initRand(1005)
    var btree = newBTreeIndex[int, string](order = 8)
    var inserted = initTable[int, seq[string]]()
    for i in 0..<200:
      let k = randKey(rng, 0, 100)
      let v = "v" & $i
      btree.insert(k, v)
      if k notin inserted:
        inserted[k] = @[]
      inserted[k].add(v)
    let beforeSize = btree.len
    var removedCount = 0
    for k, vals in inserted:
      if vals.len > 0:
        btree.remove(k, vals[0])
        inc removedCount
    # Size should decrease by number of keys that had values removed
    # (if all values removed, key is deleted)
    check btree.len <= beforeSize

  test "B-Tree with large order handles many inserts":
    var rng = initRand(1006)
    var btree = newBTreeIndex[int, string](order = 64)
    for i in 0..<2000:
      btree.insert(i, "v" & $i)
    check btree.len == 2000
    for i in 0..<2000:
      check btree.contains(i)

  test "B-Tree duplicate inserts append values":
    var rng = initRand(1007)
    var btree = newBTreeIndex[int, string](order = 8)
    let k = 42
    for i in 0..<50:
      btree.insert(k, "v" & $i)
    let vals = btree.get(k)
    check vals.len == 50
    for i in 0..<50:
      check vals[i] == "v" & $i

  test "B-Tree scan on empty tree returns empty":
    var btree = newBTreeIndex[int, string]()
    let result = btree.scan(0, 100)
    check result.len == 0

  test "B-Tree random interleaved insert/remove maintains invariants":
    var rng = initRand(1008)
    var btree = newBTreeIndex[int, string](order = 8)
    var tracker = initTable[int, seq[string]]()
    for i in 0..<300:
      let op = rng.rand(0..2)
      let k = randKey(rng, 0, 50)
      case op
      of 0, 1:  # insert
        let v = "v" & $i
        btree.insert(k, v)
        if k notin tracker:
          tracker[k] = @[]
        tracker[k].add(v)
      of 2:  # remove
        if k in tracker and tracker[k].len > 0:
          let v = tracker[k][0]
          btree.remove(k, v)
          tracker[k].del(0)
          if tracker[k].len == 0:
            tracker.del(k)
      else: discard
    # Verify all tracked keys are present (compare as sets, order may differ due to rebalancing)
    for k, vals in tracker:
      let got = btree.get(k)
      check got.toHashSet == vals.toHashSet
