/**
 * @module tests/integration/rollback-trapdoor
 * @description Regression for a consumer-reported data-integrity bug: a failed
 * CANONICAL rollback undo left the record durable while the request errored (a
 * post-commit response lie). Reproduced by a failure-injection coherence harness:
 * a late index op throws → rollback runs → an earlier op's canonical undo
 * (storage.deleteNounMetadata) fails persistently → the record stays on disk
 * while Transaction.rollback threw TransactionRollbackError AND lied with
 * state='rolled_back'.
 *
 * Fix (David's ruling — adopt-forward when safe, else fail loud):
 *  - SINGLE-op add whose only damage is a durably-present orphan → adopt it
 *    forward: commit the generation (the record IS what add() wanted), return
 *    success + a loud warn; the derived index heals on rebuild/repairIndex.
 *  - MULTI-op batch, OR any restorative LOSS → StoreInconsistentError naming the
 *    unreconciled ids; the brain enters write-quarantine (reads OK, writes
 *    refused) until repairIndex() reconciles and lifts it. Transaction state is
 *    'inconsistent', never the 'rolled_back' lie.
 *
 * The injection mirrors that harness: fault index.addToIndex (trigger rollback) and
 * storage.deleteNounMetadata (fail the canonical undo).
 */
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'
import { StoreInconsistentError } from '../../src/db/errors.js'

let seq = 0
const freshId = (): string =>
  `00000000-0000-4000-8000-${(++seq).toString(16).padStart(12, '0')}`

describe('rollback trapdoor — failed canonical undo (data integrity)', () => {
  let dir: string
  let brain: any
  let restore: Array<() => void>

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

  afterEach(async () => {
    for (const r of restore) r()
    try { await brain.close() } catch { /* ignore */ }
    fs.rmSync(dir, { recursive: true, force: true })
  })

  /** Fault the metadata-index add so a write's rollback is triggered. */
  const faultIndexAdd = () => {
    const idx = brain['metadataIndex']
    const orig = idx.addToIndex.bind(idx)
    idx.addToIndex = async () => { throw new Error('injected: index add failed') }
    restore.push(() => { idx.addToIndex = orig })
  }
  /** Fault the canonical additive undo (delete of a just-created record). */
  const faultCanonicalDelete = () => {
    const storage = brain['storage']
    const orig = storage.deleteNounMetadata.bind(storage)
    storage.deleteNounMetadata = async () => { throw new Error('injected: canonical undo failed') }
    restore.push(() => { storage.deleteNounMetadata = orig })
  }

  it('ADOPT-FORWARD: a single-op add whose canonical undo fails commits as a durable, get-able record (no throw, no quarantine)', async () => {
    const id = freshId()
    const genBefore = brain.generationStore.generation()

    faultIndexAdd()        // triggers rollback
    faultCanonicalDelete() // the trapdoor: the metadata record can't be un-written

    // add() must NOT throw — the record it wanted is durable, so it is adopted.
    const returned = await brain.add({ id, data: 'orphan adopted', type: NounType.Thing })
    expect(returned).toBe(id)

    // Un-fault so reads/subsequent writes use the real methods.
    for (const r of restore) r()
    restore = []

    // The record is durable and get-able, and the generation advanced.
    expect(await brain.get(id)).not.toBeNull()
    expect(brain.generationStore.generation()).toBe(genBefore + 1)

    // The store is NOT quarantined — a subsequent write succeeds.
    const ok = await brain.add({ id: freshId(), data: 'still writable', type: NounType.Thing })
    expect(await brain.get(ok)).not.toBeNull()
  })

  it('FAIL-LOUD: a multi-op transact whose canonical undo fails throws StoreInconsistentError and write-quarantines the brain', async () => {
    // A pre-existing record to prove reads keep working under quarantine.
    const anchor = await brain.add({ id: freshId(), data: 'anchor', type: NounType.Thing })
    await brain.flush()

    faultIndexAdd()
    faultCanonicalDelete()

    const a = freshId()
    const b = freshId()
    await expect(
      brain.transact([
        { op: 'add', id: a, data: 'batch A', type: NounType.Thing },
        { op: 'add', id: b, data: 'batch B', type: NounType.Thing }
      ])
    ).rejects.toBeInstanceOf(StoreInconsistentError)

    // Writes are refused (quarantine); reads still work.
    await expect(
      brain.add({ id: freshId(), data: 'blocked', type: NounType.Thing })
    ).rejects.toThrow(/WRITE-QUARANTINED/)
    expect(await brain.get(anchor)).not.toBeNull()

    // repairIndex() reconciles and lifts the quarantine → writes resume.
    for (const r of restore) r()
    restore = []
    await brain.repairIndex()

    const ok = await brain.add({ id: freshId(), data: 'writable again', type: NounType.Thing })
    expect(await brain.get(ok)).not.toBeNull()
  })

  it('the StoreInconsistentError names the unreconciled records with dispositions', async () => {
    faultIndexAdd()
    faultCanonicalDelete()

    const a = freshId()
    const b = freshId()
    let caught: unknown
    await brain
      .transact([
        { op: 'add', id: a, data: 'A', type: NounType.Thing },
        { op: 'add', id: b, data: 'B', type: NounType.Thing }
      ])
      .catch((e: unknown) => (caught = e))

    expect(caught).toBeInstanceOf(StoreInconsistentError)
    const err = caught as StoreInconsistentError
    expect(err.records.length).toBeGreaterThan(0)
    // Every unreconciled record here is a durably-present orphan.
    expect(err.records.every((r) => r.disposition === 'orphan')).toBe(true)
    expect(err.message).toMatch(/WRITE-QUARANTINED/)

    // Recover so afterEach can close cleanly.
    for (const r of restore) r()
    restore = []
    await brain.repairIndex()
  })
})
