/**
 * @module tests/integration/db-temporal
 * @description Proof suite for Brainy 8.0's temporal RANGE verbs, built on the
 * single-point `asOf()` foundation (proven byte-identical in db-mvcc). Each test
 * PROVES a stated guarantee:
 *
 *  1. asOf(g, { exclusive }) pins g−1 (strict-before); exclusive of the first
 *     commit is the empty generation 0, never a RangeError.
 *  2. since() accepts Db | generation | Date equivalently, with the direction
 *     guard; the lower bound is EXCLUSIVE.
 *  3. transactionLog({ from, to }) windows INCLUSIVELY on both ends (the contrast
 *     to since's exclusive lower bound) and applies `limit` AFTER the window.
 *  4. diff() EARNS its name: it does NOT return raw changed-ids — an id created
 *     and removed within the interval lands in NO bucket; added/removed/modified
 *     are classified by existence + a stable value comparison.
 *  5. history() ties to the trusted asOf path: every version's value deep-equals
 *     asOf(version.generation).get(id), a removal is a null value, order is oldest→newest.
 *  6. Composition: "type T changed in (g1, g2]" computed two ways (diff ids filtered
 *     to T, vs asOf(g2).find({type:T}) ∩ changed-ids) AGREE — including a where filter.
 *  7. Granularity (Model-B): single-operation writes (outside transact()) are their
 *     own immutable generation — logged, diffable, and time-travelable, exactly like
 *     a transact() of one op (the test body below proves this).
 *  8. Compaction policy contrast: diff/since THROW below the horizon; history TRUNCATES
 *     to it. (Locked so a refactor can't unify them incorrectly.)
 *
 * Mirrors the db-mvcc harness: explicit vectors (no embedder), deterministic
 * UUID-shaped ids, afterEach teardown. db-mvcc itself re-proves asOf is unchanged.
 */

import { describe, it, expect, afterEach } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { Brainy } from '../../src/brainy.js'
import { GenerationCompactedError } from '../../src/db/errors.js'
import type { GenerationStore } from '../../src/db/generationStore.js'
import { NounType } from '../../src/types/graphTypes.js'

/** Deterministic 384-dim vector so no test ever invokes the embedder. */
function vec(seed: number): number[] {
  return Array.from({ length: 384 }, (_, i) => ((seed * 31 + i * 7) % 100) / 100)
}

/** Map a label to a deterministic UUID-shaped id (storage shards by UUID hex). */
function uid(label: string): string {
  let h1 = 0x811c9dc5
  for (let i = 0; i < label.length; i++) h1 = Math.imul(h1 ^ label.charCodeAt(i), 0x01000193) >>> 0
  let h2 = 0xdeadbeef
  for (let i = label.length - 1; i >= 0; i--) h2 = Math.imul(h2 ^ label.charCodeAt(i), 0x85ebca6b) >>> 0
  const hex = h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0')
  return `00000000-0000-4000-8000-${hex.slice(0, 12)}`
}

function generationStoreOf(brain: Brainy): GenerationStore {
  return (brain as unknown as { generationStore: GenerationStore }).generationStore
}

describe('8.0 Db API — temporal range verbs', () => {
  const brains: Brainy[] = []
  const tempDirs: string[] = []

  async function openMemoryBrain(): Promise<Brainy> {
    const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
    await brain.init()
    brains.push(brain)
    return brain
  }

  afterEach(async () => {
    for (const brain of brains.splice(0)) {
      try {
        await brain.close()
      } catch {
        /* already closed */
      }
    }
    for (const dir of tempDirs.splice(0)) {
      await fs.promises.rm(dir, { recursive: true, force: true })
    }
  })

  // 1. asOf exclusive ----------------------------------------------------------
  it('asOf(g, { exclusive }) pins g−1; exclusive clamps at gen 0 without a RangeError', async () => {
    const brain = await openMemoryBrain()
    const a = uid('exc-a')
    const rAdd = await brain.transact([
      { op: 'add', id: a, type: NounType.Document, data: 'a', vector: vec(1), metadata: { v: 1 } }
    ])
    const gAdd = rAdd.generation
    await rAdd.release()
    const rUpd = await brain.transact([{ op: 'update', id: a, metadata: { v: 2 } }])
    const gUpd = rUpd.generation
    await rUpd.release()

    // exclusive of gUpd = the state immediately before it (= the add generation, v:1)
    const before = await brain.asOf(gUpd, { exclusive: true })
    expect(before.generation).toBe(gUpd - 1)
    expect((await before.get(a))?.metadata?.v).toBe(1)
    await before.release()

    const at = await brain.asOf(gUpd) // inclusive
    expect((await at.get(a))?.metadata?.v).toBe(2)
    await at.release()

    // exclusive clamps at 0 (never a RangeError): strict-before generation 1 is the
    // empty pre-commit state — `a` does not exist there.
    const zero = await brain.asOf(1, { exclusive: true })
    expect(zero.generation).toBe(0)
    expect(await zero.get(a)).toBeNull()
    await zero.release()
  })

  // 2. since 3-forms + direction guard ----------------------------------------
  it('since() accepts Db | generation | Date equivalently; lower bound is exclusive; direction guarded', async () => {
    const brain = await openMemoryBrain()
    const a = uid('since-a')
    const b = uid('since-b')
    const r1 = await brain.transact([
      { op: 'add', id: a, type: NounType.Document, data: 'a', vector: vec(1), metadata: {} }
    ])
    const g1 = r1.generation // a's add generation
    await brain.transact([
      { op: 'add', id: b, type: NounType.Document, data: 'b', vector: vec(2), metadata: {} }
    ])
    await brain.transact([{ op: 'update', id: a, metadata: { x: 1 } }])

    const now = brain.now()
    const viaDb = await now.since(r1) // changes in (g1, now]
    const viaGen = await now.since(g1)
    const viaEpoch = await now.since(new Date(0)) // resolves to gen 0 → (0, now]

    // since(db) === since(db.generation); the EXCLUSIVE lower bound omits a's own add@g1
    expect(viaDb).toEqual(viaGen)
    expect(viaDb.fromGeneration).toBe(g1)
    expect(viaDb.nouns).toEqual([a, b].sort()) // a (updated after g1) + b (added after g1)
    expect(viaEpoch.nouns).toEqual([a, b].sort()) // (0, now] also includes a's creation, still {a, b}

    // direction guard: an older view cannot be `since` a newer lower bound
    const older = await brain.asOf(1)
    await expect(older.since(now)).rejects.toBeInstanceOf(RangeError)
    await older.release()
    await now.release()
    await r1.release()
  })

  // 3. transactionLog window (inclusive both ends) + limit-after-window --------
  it('transactionLog({ from, to }) windows inclusively on both ends and applies limit last', async () => {
    const brain = await openMemoryBrain()
    const gens: number[] = []
    for (let v = 1; v <= 5; v++) {
      const r = await brain.transact([
        {
          op: 'add',
          id: uid(`log-${v}`),
          type: NounType.Document,
          data: `d${v}`,
          vector: vec(v),
          metadata: { v }
        }
      ])
      gens.push(r.generation) // 5 consecutive generations, one log entry each
      await r.release()
    }

    const all = await brain.transactionLog()
    expect(all.map((e) => e.generation)).toEqual([...gens].reverse()) // newest first

    // INCLUSIVE both ends — gens[1] AND gens[3] are present (contrast since's exclusive lower).
    const windowed = await brain.transactionLog({ from: gens[1], to: gens[3] })
    expect(windowed.map((e) => e.generation)).toEqual([gens[3], gens[2], gens[1]])

    // limit applies AFTER the window (newest first within the window)
    const limited = await brain.transactionLog({ from: gens[1], to: gens[3], limit: 2 })
    expect(limited.map((e) => e.generation)).toEqual([gens[3], gens[2]])

    // Inclusivity CONTRAST vs since: transactionLog INCLUDES `from`; since EXCLUDES it.
    const logFrom = await brain.transactionLog({ from: gens[1] }) // [gens[1], …]
    expect(logFrom.map((e) => e.generation).includes(gens[1])).toBe(true)
    const sinceFrom = await brain.now().since(gens[1]) // (gens[1], …] — exclusive lower
    expect(sinceFrom.fromGeneration).toBe(gens[1])

    await expect(
      brain.transactionLog({ from: gens[3], to: gens[1] })
    ).rejects.toBeInstanceOf(RangeError)
  })

  // 4. diff EARNS its name -----------------------------------------------------
  it('diff() classifies added/removed/modified — and an id created+removed within the interval is in NO bucket', async () => {
    const brain = await openMemoryBrain()
    const persistent = uid('diff-persistent')
    const created = uid('diff-created')
    const removed = uid('diff-removed')
    const ephemeral = uid('diff-ephemeral')

    // gen 1: baseline — persistent + removed exist
    await (
      await brain.transact([
        { op: 'add', id: persistent, type: NounType.Document, data: 'p', vector: vec(1), metadata: { v: 1 } },
        { op: 'add', id: removed, type: NounType.Document, data: 'r', vector: vec(2), metadata: { v: 1 } }
      ])
    ).release()
    const g1 = brain.generation()

    // gen 2: create `ephemeral` and `created`; modify `persistent`; delete `removed`
    await (
      await brain.transact([
        { op: 'add', id: ephemeral, type: NounType.Document, data: 'e', vector: vec(3), metadata: { v: 1 } },
        { op: 'add', id: created, type: NounType.Document, data: 'c', vector: vec(4), metadata: { v: 1 } },
        { op: 'update', id: persistent, metadata: { v: 2 } },
        { op: 'remove', id: removed }
      ])
    ).release()
    // gen 3: delete `ephemeral` (so it was BORN and DIED inside (g1, g3])
    await (await brain.transact([{ op: 'remove', id: ephemeral }])).release()
    const g3 = brain.generation()

    const d = await brain.diff(g1, g3)
    expect(d.added.nouns).toEqual([created]) // exists at g3, not g1
    expect(d.removed.nouns).toEqual([removed]) // exists at g1, not g3
    expect(d.modified.nouns).toEqual([persistent]) // exists both, value changed
    // EARNS-ITS-NAME: `ephemeral` was touched (raw changedBetween would list it) but is
    // absent at BOTH endpoints → it appears in NONE of the buckets.
    expect(d.added.nouns).not.toContain(ephemeral)
    expect(d.removed.nouns).not.toContain(ephemeral)
    expect(d.modified.nouns).not.toContain(ephemeral)

    // Orientation: diff(g3, g1) flips added/removed.
    const rev = await brain.diff(g3, g1)
    expect(rev.added.nouns).toEqual([removed])
    expect(rev.removed.nouns).toEqual([created])
    expect(rev.modified.nouns).toEqual([persistent])

    // diff(g, g) is empty.
    const empty = await brain.diff(g3, g3)
    expect(empty.added.nouns).toEqual([])
    expect(empty.modified.nouns).toEqual([])
  })

  // 5. history cross-checks the trusted asOf path ------------------------------
  it('history() — each version deep-equals asOf(version.generation).get(id); removal is a null value', async () => {
    const brain = await openMemoryBrain()
    const a = uid('hist-a')
    await (
      await brain.transact([
        { op: 'add', id: a, type: NounType.Document, data: 'a', vector: vec(1), metadata: { v: 1 } }
      ])
    ).release() // gen 1
    await (await brain.transact([{ op: 'update', id: a, metadata: { v: 2 } }])).release() // gen 2
    await (await brain.transact([{ op: 'update', id: a, metadata: { v: 3 } }])).release() // gen 3
    await (await brain.transact([{ op: 'remove', id: a }])).release() // gen 4

    const h = await brain.history(a)
    expect(h.kind).toBe('noun')
    // oldest → newest, ending in a removal (null value)
    const vs = h.versions
    expect(vs.length).toBeGreaterThanOrEqual(4)
    for (let i = 1; i < vs.length; i++) {
      expect(vs[i].generation).toBeGreaterThan(vs[i - 1].generation) // strictly ascending
    }
    expect(vs[vs.length - 1].value).toBeNull() // removed at the end

    // Each version ties to the trusted asOf path.
    for (const v of vs) {
      const db = await brain.asOf(v.generation)
      const viaAsOf = await db.get(a)
      await db.release()
      if (v.value === null) expect(viaAsOf).toBeNull()
      else expect(viaAsOf).toEqual(v.value)
    }

    // The distinct user-visible values appear in order.
    const versionsWithValue = vs.filter((v) => v.value !== null)
    expect(versionsWithValue.map((v) => (v.value as any).metadata?.v)).toEqual([1, 2, 3])

    // Unknown id → empty history (no throw).
    const unknown = await brain.history(uid('hist-nobody'))
    expect(unknown.versions).toEqual([])
  })

  // 6. Composition proof -------------------------------------------------------
  it('composition: "type T changed in (g1, g2]" agrees computed via diff vs via asOf(g2).find ∩ changed', async () => {
    const brain = await openMemoryBrain()
    const p1 = uid('comp-p1')
    const p2 = uid('comp-p2')
    const p3 = uid('comp-p3')
    const d1 = uid('comp-d1')

    await (
      await brain.transact([
        { op: 'add', id: p1, type: NounType.Person, data: 'p1', vector: vec(1), metadata: { team: 'x' } },
        { op: 'add', id: d1, type: NounType.Document, data: 'd1', vector: vec(2), metadata: { team: 'x' } }
      ])
    ).release()
    const g1 = brain.generation()
    await (
      await brain.transact([
        { op: 'add', id: p2, type: NounType.Person, data: 'p2', vector: vec(3), metadata: { team: 'x' } },
        { op: 'update', id: d1, metadata: { team: 'y' } }
      ])
    ).release()
    await (
      await brain.transact([
        { op: 'add', id: p3, type: NounType.Person, data: 'p3', vector: vec(4), metadata: { team: 'z' } }
      ])
    ).release()
    const g2 = brain.generation()

    // Way 1 — from diff: ids that came-into-being or changed in (g1, g2], filtered to Person at g2.
    const d = await brain.diff(g1, g2)
    const changedNouns = [...d.added.nouns, ...d.modified.nouns]
    const dbAtG2 = await brain.asOf(g2)
    const way1: string[] = []
    for (const id of changedNouns) {
      const e = await dbAtG2.get(id)
      if (e?.type === NounType.Person) way1.push(id)
    }

    // Way 2 — from find ∩ changed: all Person at g2, intersect with the changed-id set.
    const personsAtG2 = await dbAtG2.find({ type: NounType.Person, limit: 100 })
    const changedSet = new Set([...d.added.nouns, ...d.removed.nouns, ...d.modified.nouns])
    const way2 = personsAtG2.map((r) => r.id).filter((id) => changedSet.has(id))

    expect(way1.sort()).toEqual(way2.sort())
    expect(way1.sort()).toEqual([p2, p3].sort()) // p2 added, p3 added; p1 unchanged; d1 is a Document

    // With a where filter: Person on team 'x' changed in (g1, g2] → just p2.
    const personsTeamX = await dbAtG2.find({ type: NounType.Person, where: { team: 'x' }, limit: 100 })
    const teamXChanged = personsTeamX.map((r) => r.id).filter((id) => changedSet.has(id))
    expect(teamXChanged).toEqual([p2])

    await dbAtG2.release()
  })

  // 7. Granularity (Model-B) ---------------------------------------------------
  it('granularity: single-operation writes ARE versioned and visible to the temporal verbs', async () => {
    const brain = await openMemoryBrain()
    const a = uid('gran-a')
    const r1 = await brain.transact([
      { op: 'add', id: a, type: NounType.Document, data: 'a', vector: vec(1), metadata: { v: 1 } }
    ])
    await r1.release()
    expect((await brain.transactionLog()).length).toBe(1)

    // Model-B: a single-op write is its OWN immutable generation — logged,
    // diffable, and time-travelable, exactly like a transact() of one op.
    await brain.update({ id: a, metadata: { v: 2 } })

    // The single-op update appended a generation/log entry.
    expect((await brain.transactionLog()).length).toBe(2)
    expect(brain.generation()).toBe(r1.generation + 1)

    // diff sees the single-op update as a modification of `a`.
    const d = await brain.diff(r1.generation, brain.generation())
    expect(d.added.nouns).toEqual([])
    expect(d.modified.nouns).toEqual([a])

    // And asOf() resolves both states: v=1 at the transact gen, v=2 at the single-op gen.
    const atTransact = await brain.asOf(r1.generation)
    const atSingleOp = await brain.asOf(brain.generation())
    expect((await atTransact.get(a))?.metadata?.v).toBe(1)
    expect((await atSingleOp.get(a))?.metadata?.v).toBe(2)
    await atTransact.release()
    await atSingleOp.release()
  })

  // 8. Compaction policy contrast ---------------------------------------------
  it('compaction: diff/since THROW below the horizon; history TRUNCATES to it', async () => {
    const brain = await openMemoryBrain()
    const a = uid('comp-a')
    for (let v = 1; v <= 6; v++) {
      await (
        await brain.transact([
          v === 1
            ? { op: 'add', id: a, type: NounType.Document, data: 'a', vector: vec(v), metadata: { v } }
            : { op: 'update', id: a, metadata: { v } }
        ])
      ).release()
    } // gens 1..6, all pins released

    await brain.compactHistory({ maxGenerations: 2 }) // raise the horizon
    const horizon = generationStoreOf(brain).horizon()
    expect(horizon).toBeGreaterThan(0)

    // diff / since reaching below the horizon THROW.
    await expect(brain.diff(0, 6)).rejects.toBeInstanceOf(GenerationCompactedError)
    const now = brain.now()
    await expect(now.since(0)).rejects.toBeInstanceOf(GenerationCompactedError)
    await now.release()

    // history TRUNCATES (no throw) — its versions start at or above the horizon.
    const h = await brain.history(a)
    expect(h.versions.length).toBeGreaterThan(0)
    for (const v of h.versions) expect(v.generation).toBeGreaterThanOrEqual(horizon)
  })
})
