/**
 * @module tests/integration/migration-backup
 * @description Proof suite for the pre-upgrade backup (#42): before a one-time
 * 7.x → 8.0 rebuild mutates the derived indexes, brainy hard-link-snapshots the
 * brain directory (default-on, opt-out via `migrationBackup: false`), removes it
 * once the upgrade verifies + stamps, and retains it on failure for rollback.
 *
 * Covers: the FileSystemStorage machinery (hard-link snapshot, reuse, remove
 * without touching live data, empty→null); the brainy lifecycle on a stale-epoch
 * reopen (backup created then removed on success); the opt-out; and the memory
 * feature-detect (no machinery → graceful no-op).
 *
 * Staleness is forced by stamping a marker with a mismatched `indexEpoch` (what a
 * pre-8.0 brain looks like), via the same `writeRawObject` surface the marker is
 * stored through — not by deleting an on-disk file (the marker path is encoded).
 */
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 { FileSystemStorage } from '../../src/storage/adapters/fileSystemStorage.js'
import { NounType } from '../../src/types/graphTypes.js'

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

/** Build a persisted brain with one entity. `staleAfter` overwrites the marker
 *  with a mismatched epoch so the NEXT open detects a 7.x→8.0 upgrade. */
async function buildBrainWithData(dir: string, staleAfter = false): Promise<string> {
  const brain: any = new Brainy({
    requireSubtype: false,
    storage: { type: 'filesystem', path: dir },
    silent: true
  })
  await brain.init()
  const id = await brain.add({ data: 'protect me during the upgrade', type: NounType.Concept })
  await brain.flush()
  if (staleAfter) {
    await brain.storage.writeRawObject('_system/brain-format.json', { dataFormat: '7.0', indexEpoch: 0 })
  }
  await brain.close()
  return id
}

describe('Pre-migration backup (#42)', () => {
  it('byte-copies _id_mapper/* (mmap, mutated in place) but hard-links the rest', async () => {
    // cor confirmed the native _id_mapper mmap is msync/truncate-in-place, so a
    // hard-linked snapshot of it would be corrupted by a live mutation reaching
    // through the shared inode — it must be byte-copied. Everything else (written
    // tmp+rename) hard-links safely.
    const dir = mkTmp()
    await buildBrainWithData(dir)
    fs.mkdirSync(path.join(dir, '_id_mapper'), { recursive: true })
    fs.writeFileSync(path.join(dir, '_id_mapper', 'map.bin'), Buffer.from([1, 2, 3, 4]))
    fs.writeFileSync(path.join(dir, 'regular.bin'), Buffer.from([9, 9, 9, 9]))

    const storage: any = new FileSystemStorage(dir)
    await storage.init()
    const backup = await storage.createMigrationBackup()

    // _id_mapper file → byte-copied: DIFFERENT inode (a live msync can't reach it) + faithful content
    const mapperLive = fs.statSync(path.join(dir, '_id_mapper', 'map.bin')).ino
    const mapperBackup = fs.statSync(path.join(backup, '_id_mapper', 'map.bin')).ino
    expect(mapperBackup).not.toBe(mapperLive)
    expect(fs.readFileSync(path.join(backup, '_id_mapper', 'map.bin'))).toEqual(Buffer.from([1, 2, 3, 4]))

    // a regular (tmp+rename) file → hard-linked: SAME inode (zero-copy)
    const regLive = fs.statSync(path.join(dir, 'regular.bin')).ino
    const regBackup = fs.statSync(path.join(backup, 'regular.bin')).ino
    expect(regBackup).toBe(regLive)
  })

  it('FileSystemStorage.createMigrationBackup hard-links the store; removeMigrationBackup deletes it without touching live data', async () => {
    const dir = mkTmp()
    const id = await buildBrainWithData(dir)

    const storage: any = new FileSystemStorage(dir)
    await storage.init()

    const backupPath = await storage.createMigrationBackup()
    expect(backupPath).toBe(`${dir}.migration-backup`)
    expect(fs.existsSync(backupPath)).toBe(true)
    expect(fs.readdirSync(backupPath).length).toBeGreaterThan(0) // snapshot has content

    // Reuse: a second call returns the same path (a prior failed upgrade's
    // pre-state is preserved, not overwritten) and does not throw.
    expect(await storage.createMigrationBackup()).toBe(backupPath)

    // Remove: gone from disk, and the LIVE store is untouched (shared inodes).
    await storage.removeMigrationBackup(backupPath)
    expect(fs.existsSync(backupPath)).toBe(false)

    const reopened: any = new Brainy({
      requireSubtype: false,
      storage: { type: 'filesystem', path: dir },
      silent: true
    })
    await reopened.init()
    expect(await reopened.get(id)).not.toBeNull() // live data survived the backup+remove
    await reopened.close()
  })

  it('returns null for an empty store — nothing to protect', async () => {
    const dir = mkTmp()
    const storage: any = new FileSystemStorage(dir) // raw store, no brain → no VFS root
    await storage.init()
    expect(await storage.createMigrationBackup()).toBeNull()
  })

  it('takes a backup on a stale-epoch reopen (default-on) and removes it once the upgrade verifies', async () => {
    const dir = mkTmp()
    await buildBrainWithData(dir, /* staleAfter */ true)

    const created: string[] = []
    const removed: string[] = []
    const proto = FileSystemStorage.prototype as any
    const origCreate = proto.createMigrationBackup
    const origRemove = proto.removeMigrationBackup
    proto.createMigrationBackup = async function () {
      const p = await origCreate.call(this)
      if (p) created.push(p)
      return p
    }
    proto.removeMigrationBackup = async function (p: string) {
      removed.push(p)
      return origRemove.call(this, p)
    }
    try {
      const brain: any = new Brainy({
        requireSubtype: false,
        storage: { type: 'filesystem', path: dir },
        silent: true
      })
      await brain.init() // epochStale → backup created; inline rebuild stamps → backup removed
      await brain.close()

      expect(created.length).toBe(1) // a backup WAS taken before the rebuild
      expect(removed).toContain(created[0]) // and removed once the upgrade stamped
      expect(fs.existsSync(created[0])).toBe(false) // gone from disk
    } finally {
      proto.createMigrationBackup = origCreate
      proto.removeMigrationBackup = origRemove
    }
  })

  it('opt-out (migrationBackup:false) takes no backup; memory storage is a graceful no-op', async () => {
    const dir = mkTmp()
    await buildBrainWithData(dir, /* staleAfter */ true)

    const proto = FileSystemStorage.prototype as any
    const origCreate = proto.createMigrationBackup
    let calls = 0
    proto.createMigrationBackup = async function () {
      calls++
      return origCreate.call(this)
    }
    try {
      const brain: any = new Brainy({
        requireSubtype: false,
        storage: { type: 'filesystem', path: dir },
        migrationBackup: false,
        silent: true
      })
      await brain.init()
      await brain.close()
      expect(calls).toBe(0)
    } finally {
      proto.createMigrationBackup = origCreate
    }

    // Memory storage has no backup machinery — default-on must not crash.
    const mem: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
    await mem.init()
    expect(mem.isInitialized).toBe(true)
    expect(mem.config.migrationBackup).toBe(true) // default resolved
    await mem.close()
  })
})
