/**
 * @module tests/integration/restore-nondestructive
 * @description Regression for a consumer-reported recovery hazard: `restore()`
 * removed the entire live brain directory and THEN `fs.cp`'d the snapshot in, so
 * a copy failure (most dangerously ENOSPC — `fs.cp` also materializes the holes
 * of sparse mmap blobs, ballooning a snapshot that would otherwise fit) left the
 * store destroyed with only a partial copy: the recovery tool could destroy the
 * brain it was meant to recover.
 *
 * Fix: the snapshot is copied into a staging area (sparse-aware) BEFORE any live
 * data is touched; only after the copy succeeds and a marker is fsync'd does an
 * atomic per-entry swap move it into place. A copy failure leaves the live store
 * exactly as it was; a crash mid-swap is resumed forward on the next open.
 *
 * These tests exercise the property directly: a forced copy failure leaves live
 * data intact, a normal restore round-trips correctly, an interrupted-but-
 * committed staging area is completed on reopen, and the sparse copy preserves
 * holes byte-for-byte.
 */
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'
import { NounType } from '../../src/types/graphTypes.js'

const STAGING = '_restore_staging'
const MARKER = '.restore-manifest.json'

describe('non-destructive restore (BRAINY-RESTORE-DESTRUCTIVE)', () => {
  let root: string
  let snap: string
  let brain: any

  const openBrain = async (dir: string) => {
    const b = new Brainy({
      requireSubtype: false,
      storage: { type: 'filesystem', path: dir },
      dimensions: 384,
      silent: true
    })
    await b.init()
    return b
  }

  beforeEach(async () => {
    process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
    const base = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-restore-'))
    root = path.join(base, 'store')
    snap = path.join(base, 'snapshot')
    brain = await openBrain(root)
  })

  afterEach(async () => {
    try { await brain.close() } catch { /* already closed in a test */ }
    fs.rmSync(path.dirname(root), { recursive: true, force: true })
  })

  it('a copy failure leaves the LIVE store completely intact (the core property)', async () => {
    const keep = await brain.add({ id: '00000000-0000-4000-8000-0000000000c1', data: 'in snapshot', type: NounType.Thing })
    await brain.flush()
    await brain['storage'].snapshotToDirectory(snap)
    // Mutate live AFTER the snapshot — this state must survive a failed restore.
    const extra = await brain.add({ id: '00000000-0000-4000-8000-0000000000c2', data: 'live only', type: NounType.Thing })

    // Force the staging copy to fail on its second entry (partial staging).
    const storage = brain['storage']
    const realCopy = storage.copyTreeSparse.bind(storage)
    let calls = 0
    storage.copyTreeSparse = async (src: string, dest: string) => {
      if (++calls === 2) throw new Error('simulated ENOSPC')
      return realCopy(src, dest)
    }

    await expect(brain.restore(snap, { confirm: true })).rejects.toThrow(/untouched/)

    // Live data — BOTH the snapshot entity and the live-only mutation — survives.
    expect(await brain.get(keep)).not.toBeNull()
    expect(await brain.get(extra)).not.toBeNull()
    // The half-written staging area was cleaned up.
    expect(fs.existsSync(path.join(root, STAGING))).toBe(false)
  })

  it('a normal restore round-trips to the snapshot state', async () => {
    const a = await brain.add({ id: '00000000-0000-4000-8000-0000000000c3', data: 'A', type: NounType.Thing })
    await brain.flush()
    await brain['storage'].snapshotToDirectory(snap)
    // Diverge from the snapshot: add B, remove A.
    const b = await brain.add({ id: '00000000-0000-4000-8000-0000000000c4', data: 'B', type: NounType.Thing })
    await brain.remove(a)
    expect(await brain.get(a)).toBeNull()
    expect(await brain.get(b)).not.toBeNull()

    await brain.restore(snap, { confirm: true })

    // Back to exactly the snapshot: A present, B gone.
    expect(await brain.get(a)).not.toBeNull()
    expect(await brain.get(b)).toBeNull()
    expect(fs.existsSync(path.join(root, STAGING))).toBe(false)
  })

  it('an interrupted-but-committed restore is completed on the next open (resume forward)', async () => {
    const a = await brain.add({ id: '00000000-0000-4000-8000-0000000000c5', data: 'A', type: NounType.Thing })
    await brain.flush()
    await brain['storage'].snapshotToDirectory(snap)
    const b = await brain.add({ id: '00000000-0000-4000-8000-0000000000c6', data: 'B', type: NounType.Thing })
    await brain.flush()
    await brain.close()

    // Simulate the state right after the staging copy committed but before the
    // swap ran: a _restore_staging holding the snapshot's entries + the marker.
    const staging = path.join(root, STAGING)
    fs.mkdirSync(staging, { recursive: true })
    const entries: string[] = []
    for (const entry of fs.readdirSync(snap)) {
      if (entry === 'locks' || entry === STAGING) continue
      fs.cpSync(path.join(snap, entry), path.join(staging, entry), { recursive: true })
      entries.push(entry)
    }
    fs.writeFileSync(path.join(staging, MARKER), JSON.stringify({ entries }))

    // Reopen → init resumes the swap → the store is the snapshot (A, no B).
    brain = await openBrain(root)
    expect(await brain.get(a)).not.toBeNull()
    expect(await brain.get(b)).toBeNull()
    expect(fs.existsSync(staging)).toBe(false)
  })

  it('an uncommitted staging area (no marker) is discarded on open, live untouched', async () => {
    const a = await brain.add({ id: '00000000-0000-4000-8000-0000000000c7', data: 'A live', type: NounType.Thing })
    await brain.flush()
    await brain.close()

    // A staging dir with NO marker = a copy that never committed → debris.
    const staging = path.join(root, STAGING)
    fs.mkdirSync(staging, { recursive: true })
    fs.writeFileSync(path.join(staging, 'entities'), 'garbage partial copy')

    brain = await openBrain(root)
    expect(await brain.get(a)).not.toBeNull() // live authoritative
    expect(fs.existsSync(staging)).toBe(false) // debris removed
  })

  it('the sparse copy preserves holes: content byte-identical, allocation far below apparent size', async () => {
    const base = path.dirname(root)
    const src = path.join(base, 'sparse-src.bin')
    const dest = path.join(base, 'sparse-dest.bin')
    const SIZE = 32 * 1024 * 1024 // 32 MiB apparent
    const DATA = Buffer.from('hello sparse world')

    // Create a sparse source: 32 MiB of holes with a little real data at the start.
    const fh = fs.openSync(src, 'w')
    fs.ftruncateSync(fh, SIZE)
    fs.writeSync(fh, DATA, 0, DATA.length, 0)
    fs.closeSync(fh)

    const storage = brain['storage']
    const stat = fs.statSync(src)
    await storage.copyFileSparse(src, dest, stat.size, stat.mode)

    // Same apparent size and byte-identical content…
    const destStat = fs.statSync(dest)
    expect(destStat.size).toBe(SIZE)
    expect(fs.readFileSync(dest).equals(fs.readFileSync(src))).toBe(true)
    // …but the destination is actually sparse (allocated bytes far below size).
    expect(destStat.blocks * 512).toBeLessThan(SIZE / 2)
  })
})
