/**
 * @module tests/integration/temporal-vfs
 * @description VFS content joins the Model-B immutability model. Previously
 * the temporal model had a hole exactly where files were concerned: entity
 * records were versioned per write (before-images), but the content BYTES
 * lived under an eager refCount GC — `rm` could physically delete bytes that
 * in-window history still referenced, and overwrite never released the old
 * hash at all (an unbounded silent leak that only accidentally preserved
 * history). Now blob bytes are retention-protected: a content blob referenced
 * by any generation inside the retention window (or live) survives, and the
 * ONE reclamation point is history compaction.
 *
 * Pins:
 *  - `readFile(path, { asOf })` returns each version's exact bytes.
 *  - `history(path)` lists versions ascending with generation/hash/size.
 *  - overwrite releases the superseded live reference (leak fixed) while
 *    history protects the bytes; rm keeps bytes readable via asOf.
 *  - compaction past the last referencing generation physically reclaims
 *    bytes (zero live + zero history), and never reclaims in-window content —
 *    including the cross-file dedup case where an old file's history and a
 *    newer file's history share one hash.
 *  - overwrite refreshes the entity's `data` (embedding text) — the stale
 *    `.data` defect.
 *  - the backfill/scrub recounts exactly from the generation records.
 */
import { describe, it, expect, beforeEach, 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'

describe('temporal VFS — blob immutability under Model B', () => {
  let dir: string
  let brain: any

  beforeEach(async () => {
    process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
    dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-temporal-vfs-'))
    brain = new Brainy({
      requireSubtype: false,
      storage: { type: 'filesystem', path: dir },
      dimensions: 384,
      silent: true
    })
    await brain.init()
  })

  afterEach(async () => {
    await brain.close()
    fs.rmSync(dir, { recursive: true, force: true })
  })

  /** The file's blob metadata via the storage layer (test introspection). */
  async function blobMeta(hash: string) {
    return brain['storage'].blobStorage.getMetadata(hash)
  }

  it('readFile(path, {asOf}) returns each version‘s exact bytes; history(path) lists them', async () => {
    const p = '/pages/home.json'
    await brain.vfs.writeFile(p, 'v1 — original')
    const g1 = brain.generationStore.generation()
    await brain.vfs.writeFile(p, 'v2 — edited')
    await brain.vfs.writeFile(p, 'v3 — final')

    // Live read = newest.
    expect((await brain.vfs.readFile(p)).toString()).toBe('v3 — final')

    // History lists the file's write-generations ascending, distinct hashes.
    const versions = await brain.vfs.history(p)
    expect(versions.length).toBeGreaterThanOrEqual(3)
    const gens = versions.map((v: any) => v.generation)
    expect([...gens].sort((a: number, b: number) => a - b)).toEqual(gens)

    // Every listed version's exact bytes are readable.
    const texts: string[] = []
    for (const v of versions) {
      texts.push((await brain.vfs.readFile(p, { asOf: v.generation })).toString())
    }
    expect(texts).toContain('v1 — original')
    expect(texts).toContain('v2 — edited')
    expect(texts).toContain('v3 — final')

    // Direct generation read too (the incident shape: recover the past bytes).
    expect((await brain.vfs.readFile(p, { asOf: g1 })).toString()).toBe('v1 — original')
  })

  it('overwrite releases the superseded LIVE reference (leak fixed) while history protects the bytes', async () => {
    const p = '/notes/leak.txt'
    await brain.vfs.writeFile(p, 'old content A')
    const oldHash = (await brain.vfs.history(p))[0].hash
    const g1 = brain.generationStore.generation()

    await brain.vfs.writeFile(p, 'new content B')

    // The old hash's LIVE reference is released (previously it leaked forever)…
    expect((await blobMeta(oldHash))?.refCount).toBe(0)
    // …but the bytes survive (history-protected) and asOf still reads them.
    expect((await brain.vfs.readFile(p, { asOf: g1 })).toString()).toBe('old content A')
  })

  it('rm keeps the bytes readable via asOf inside the window (no premature deletion)', async () => {
    const p = '/docs/doomed.txt'
    await brain.vfs.writeFile(p, 'still recoverable after rm')
    const g = brain.generationStore.generation()
    const hash = (await brain.vfs.history(p))[0].hash

    await brain.vfs.unlink(p)

    // Live path is gone…
    await expect(brain.vfs.readFile(p)).rejects.toThrow()
    // …the bytes are not: zero live refs, history-protected.
    expect((await blobMeta(hash))?.refCount).toBe(0)
    expect((await brain['storage'].blobStorage.read(hash)).toString()).toBe(
      'still recoverable after rm'
    )
    // (Path-level asOf resolution of a DELETED path is future work — the
    // bytes + entity history are intact; recovery reads go through the hash
    // or a pre-delete generation view.)
    void g
  })

  it('compaction is the ONE reclamation point: past-window bytes are reclaimed, in-window preserved', async () => {
    const p = '/pages/compact.json'
    await brain.vfs.writeFile(p, 'version ONE')
    const v1Hash = (await brain.vfs.history(p))[0].hash
    await brain.vfs.writeFile(p, 'version TWO')
    await brain.vfs.writeFile(p, 'version THREE')

    // Sanity: v1's bytes exist (history-protected, live-released).
    expect(await blobMeta(v1Hash)).toBeDefined()
    expect((await blobMeta(v1Hash))?.refCount).toBe(0)

    // Compact everything reclaimable (retain nothing beyond the live state).
    await brain.compactHistory({ maxGenerations: 0 })

    // v1's bytes are physically GONE — zero live, zero history.
    expect(await blobMeta(v1Hash)).toBeUndefined()
    // The live version is untouched.
    expect((await brain.vfs.readFile(p)).toString()).toBe('version THREE')
  })

  it('cross-file dedup: a shared hash survives while ANY in-window generation references it', async () => {
    const shared = 'identical bytes shared across files and time'
    const pA = '/a.txt'
    const pB = '/b.txt'

    // A holds the shared content, then moves off it (A's history references it).
    await brain.vfs.writeFile(pA, shared)
    await brain.vfs.writeFile(pA, 'A moved on')
    // B now holds the shared content live, then is removed (B's remove-gen
    // before-image references it too).
    await brain.vfs.writeFile(pB, shared)
    const gBLive = brain.generationStore.generation()
    const sharedHash = (await brain.vfs.history(pA))[0].hash
    await brain.vfs.unlink(pB)

    // Live refs are zero (A moved off; B removed) — bytes survive on history.
    expect((await blobMeta(sharedHash))?.refCount).toBe(0)
    expect((await brain['storage'].blobStorage.read(sharedHash)).toString()).toBe(shared)

    // B's content at its live generation is still exactly readable… via asOf
    // on A's early version too (same bytes, same blob).
    const versionsA = await brain.vfs.history(pA)
    const aV1 = versionsA[0]
    expect((await brain.vfs.readFile(pA, { asOf: aV1.generation })).toString()).toBe(shared)
    void gBLive

    // Full compaction drops the last history references → bytes reclaimed.
    await brain.compactHistory({ maxGenerations: 0 })
    expect(await blobMeta(sharedHash)).toBeUndefined()
  })

  it('overwrite refreshes the entity data/embedding text (the stale-.data defect)', async () => {
    const p = '/pages/data-fresh.txt'
    await brain.vfs.writeFile(p, 'first words')
    await brain.vfs.writeFile(p, 'second words entirely')

    const entityId = await brain.vfs['pathResolver'].resolve(p)
    const entity = await brain.get(entityId)
    expect(entity.data).toBe('second words entirely')
  })

  it('the scrub recounts history references exactly from the generation records', async () => {
    const p = '/pages/scrubbed.md'
    await brain.vfs.writeFile(p, 'scrub v1')
    await brain.vfs.writeFile(p, 'scrub v2')
    const v1Hash = (await brain.vfs.history(p))[0].hash
    await brain.flush()

    const storage = brain['storage']
    const before = (await blobMeta(v1Hash))?.historyRefCount ?? 0
    expect(before).toBeGreaterThan(0)

    // Corrupt the counter (simulates a legacy store / crash drift), then scrub.
    await storage.blobStorage.setHistoryRefCount(v1Hash, 0)
    const result = await storage.scrubBlobHistoryRefCounts()
    expect(result.records).toBeGreaterThan(0)
    expect((await blobMeta(v1Hash))?.historyRefCount).toBe(before)
  })
})
