/**
 * Cold-open rebuild gate — the readiness contract (8.0.12).
 *
 * A production deployment measured 48 seconds on EVERY boot because the
 * rebuild gate keyed off in-memory size()/count heuristics that are dishonest
 * for durable indexes: a disk-native provider legitimately reports 0 resident
 * entries while fully durable, and the JS graph loaded nothing at gate time
 * (its LSM initialized lazily) — so `rebuildIndexesIfNeeded()` re-derived
 * indexes from a full canonical scan on every open.
 *
 * The contract pinned here:
 *  - JS reopen: the graph must COLD-LOAD its persisted LSM (no re-derive; the
 *    old `_initializeGraphIndex` rebuilt from a full verb scan every boot),
 *    the metadata leg must not rebuild (id-mapper signal is honest), and
 *    queries stay correct. The JS vector leg still runs `rebuild()` — that IS
 *    its load path.
 *  - A vector provider exposing `isReady() === true` is never rebuilt, even
 *    at `size() === 0`; `isReady() === false` gets its rebuild. `init?()` is
 *    eagerly awaited before the gate.
 */
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, NounType, VerbType } from '../../src/index.js'
import { GraphAdjacencyIndex } from '../../src/graph/graphAdjacencyIndex.js'
import { MetadataIndexManager } from '../../src/utils/metadataIndex.js'

const tmpDirs: string[] = []
function mkTmp(): string {
  const d = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-cold-open-'))
  tmpDirs.push(d)
  return d
}
afterEach(() => {
  for (const d of tmpDirs.splice(0)) fs.rmSync(d, { recursive: true, force: true })
})

const V = () => Array.from({ length: 384 }, () => Math.random())

/** Populate a brain with nouns + edges and close it. */
async function buildBrain(dir: string, n = 12): Promise<string[]> {
  const brain: any = new Brainy({
    requireSubtype: false,
    storage: { type: 'filesystem', path: dir },
    plugins: [],
    silent: true
  })
  await brain.init()
  const ids: string[] = []
  for (let i = 0; i < n; i++) {
    ids.push(
      await brain.add({
        data: `entity ${i}`,
        type: NounType.Concept,
        subtype: 's',
        metadata: { wave: i % 3 },
        vector: V()
      })
    )
  }
  for (let i = 0; i + 1 < ids.length; i++) {
    await brain.relate({ from: ids[i], to: ids[i + 1], type: VerbType.RelatedTo, subtype: 's' })
  }
  await brain.close()
  return ids
}

describe('Cold-open rebuild gate (readiness contract)', () => {
  it('JS reopen: graph cold-loads its LSM (no re-derive), metadata skips, queries correct', async () => {
    const dir = mkTmp()
    const ids = await buildBrain(dir)

    // Spy on the two rebuilds that must NOT run on a healthy reopen.
    const graphRebuilds: number[] = []
    const metadataRebuilds: number[] = []
    const origGraphRebuild = GraphAdjacencyIndex.prototype.rebuild
    const origMetaRebuild = MetadataIndexManager.prototype.rebuild
    GraphAdjacencyIndex.prototype.rebuild = async function (...args: any[]) {
      graphRebuilds.push(1)
      return origGraphRebuild.apply(this, args as any)
    }
    MetadataIndexManager.prototype.rebuild = async function (...args: any[]) {
      metadataRebuilds.push(1)
      return origMetaRebuild.apply(this, args as any)
    }
    try {
      const brain: any = new Brainy({
        requireSubtype: false,
        storage: { type: 'filesystem', path: dir },
        plugins: [],
        silent: true
      })
      await brain.init()

      expect(graphRebuilds.length).toBe(0) // persisted LSM loaded — no O(E) re-derive
      expect(metadataRebuilds.length).toBe(0) // id-mapper signal honest — no rebuild
      expect(await brain.graphIndex.size()).toBeGreaterThan(0) // durable edges COLD-LOADED at boot

      // Correctness after the load — the whole point of not rebuilding.
      const byWhere = await brain.find({ type: NounType.Concept, where: { wave: 1 } })
      expect(byWhere.length).toBe(4) // waves 1,4,7,10 of 12
      const connected = await brain.find({ connected: { from: ids[0], depth: 1 } })
      expect(connected.length).toBe(1)
      await brain.close()
    } finally {
      GraphAdjacencyIndex.prototype.rebuild = origGraphRebuild
      MetadataIndexManager.prototype.rebuild = origMetaRebuild
    }
  })

  it('a vector provider with isReady()===true is never rebuilt, even at size()===0 (init eagerly awaited)', async () => {
    const dir = mkTmp()
    await buildBrain(dir, 6)

    const calls = { init: 0, rebuild: 0 }
    const stubVector = {
      addItem: async (item: any) => item?.id ?? 'stub-id',
      removeItem: async () => true,
      search: async () => [] as Array<[string, number]>,
      size: () => 0, // disk-native posture: durable, zero resident
      clear: () => {},
      rebuild: async () => {
        calls.rebuild++
      },
      flush: async () => 0,
      getPersistMode: () => 'deferred' as const,
      init: async () => {
        calls.init++
      },
      isReady: () => true
    }
    const brain: any = new Brainy({
      requireSubtype: false,
      storage: { type: 'filesystem', path: dir },
      plugins: [],
      silent: true
    })
    brain.use({ name: 'fake-native-vector', activate: async (ctx: any) => { ctx.registerProvider('vector', () => stubVector); return true } })
    await brain.init()

    expect(calls.init).toBe(1) // the eager cold-load ran before the gate
    expect(calls.rebuild).toBe(0) // isReady()===true replaced the size()===0 heuristic
    await brain.close()
  })

  it('a vector provider with isReady()===false gets its rebuild (honest in both directions)', async () => {
    const dir = mkTmp()
    await buildBrain(dir, 6)

    const calls = { rebuild: 0 }
    let ready = false
    const stubVector = {
      addItem: async (item: any) => item?.id ?? 'stub-id',
      removeItem: async () => true,
      search: async () => [] as Array<[string, number]>,
      size: () => 999, // even a non-zero size must NOT mask a not-ready provider
      clear: () => {},
      rebuild: async () => {
        calls.rebuild++
        ready = true // rebuild restores readiness
      },
      flush: async () => 0,
      getPersistMode: () => 'deferred' as const,
      isReady: () => ready
    }
    const brain: any = new Brainy({
      requireSubtype: false,
      storage: { type: 'filesystem', path: dir },
      plugins: [],
      silent: true
    })
    brain.use({ name: 'fake-native-vector', activate: async (ctx: any) => { ctx.registerProvider('vector', () => stubVector); return true } })
    await brain.init()

    expect(calls.rebuild).toBe(1)
    await brain.close()
  })

  it('self-heal survives: durable graph state deleted → one rebuild from canonical on reopen', async () => {
    const dir = mkTmp()
    const ids = await buildBrain(dir, 6)

    // Simulate lost durable graph state: remove the persisted LSM artifacts
    // (metadata channel keys live under _system hash buckets — nuke the graph
    // manifests via the storage API instead of guessing paths).
    const wipe: any = new Brainy({
      requireSubtype: false,
      storage: { type: 'filesystem', path: dir },
      plugins: [],
      silent: true
    })
    await wipe.init()
    // Clear both LSM trees' persisted manifests through the live index, then
    // close WITHOUT letting them re-flush a fresh manifest state.
    const gi: any = wipe.graphIndex
    await gi.lsmTreeVerbsBySource.clear?.()
    await gi.lsmTreeVerbsByTarget.clear?.()
    await wipe.close()

    const brain: any = new Brainy({
      requireSubtype: false,
      storage: { type: 'filesystem', path: dir },
      plugins: [],
      silent: true
    })
    await brain.init()
    // Canonical verbs still exist → the boot self-heal must have restored the adjacency.
    const connected = await brain.find({ connected: { from: ids[0], depth: 1 } })
    expect(connected.length).toBe(1)
    await brain.close()
  })
})
