/**
 * @module tests/integration/vfs-cow-blob-adoption
 * @description P0 recovery: a 7→8 upgrade left VFS content blobs in the removed
 * branch system's copy-on-write area (`_cow/`) instead of the 8.0
 * content-addressed store (`_cas/`), so a VFS read of a stranded blob throws
 * "Blob metadata not found" and every page backed by it 500s.
 *
 * Proves the cure end to end:
 *  - `storage.adoptLegacyCowBlobs()` copies each orphaned `_cow/` blob (bytes +
 *    metadata) into `_cas/` verbatim, idempotently, non-destructively, and
 *    reports an `incomplete` blob (bytes without metadata) rather than
 *    half-adopting it.
 *  - a real brain that upgraded into this state SELF-HEALS on its next open
 *    (auto-adoption) — the stranded VFS file reads correctly again with no
 *    operator action.
 *  - `brain.vfs.adoptOrphanedBlobs()` is the explicit equivalent.
 */
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'

const tmpDirs: string[] = []
function mkTmp(): string {
  const d = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-cow-adopt-'))
  tmpDirs.push(d)
  return d
}

/** Release a raw FileSystemStorage: flush, stop its timer, drop the writer lock
 *  so a subsequent open of the same directory is not blocked. */
async function teardownRawStorage(s: any): Promise<void> {
  try { await s.flush?.() } catch { /* best effort */ }
  try { s.stopFlushRequestWatcher?.() } catch { /* best effort */ }
  try { await s.releaseWriterLock?.() } catch { /* best effort */ }
}
afterEach(() => {
  for (const d of tmpDirs.splice(0)) fs.rmSync(d, { recursive: true, force: true })
})

/** Move every `_cas/blob*` object into `_cow/` and delete the `_cas/` copy —
 *  reproducing the exact on-disk state a 7.x brain leaves after the 8.0 layout
 *  migration collapses entities but does not adopt the copy-on-write blobs. */
async function strandCasBlobsIntoCow(dir: string): Promise<number> {
  const s: any = new FileSystemStorage(dir)
  await s.init()
  const casKeys = (await s.listRawObjects('_cas/')).filter((k: string) =>
    /(^|\/)(blob:|blob-meta:)/.test(k)
  )
  let moved = 0
  for (const full of casKeys) {
    const key = full.replace(/^_cas\//, '')
    const obj = await s.readRawObject(`_cas/${key}`)
    if (obj === null) continue
    await s.writeRawObject(`_cow/${key}`, obj)
    await s.deleteRawObject(`_cas/${key}`)
    moved++
  }
  await teardownRawStorage(s)
  return moved
}

describe('VFS _cow/ blob adoption (P0 7→8 recovery)', () => {
  it('adoptLegacyCowBlobs copies bytes + metadata into _cas/, verbatim, idempotent, non-destructive', async () => {
    const dir = mkTmp()
    const storage: any = new FileSystemStorage(dir)
    await storage.init()

    const hash = 'a'.repeat(64)
    const bytes = Buffer.from('the homepage payload')
    // 7.x layout: the pair lives ONLY under _cow/ (blob-meta carries the refCount).
    await storage.writeRawObject(`_cow/blob:${hash}`, { _binary: true, data: bytes.toString('base64') })
    await storage.writeRawObject(`_cow/blob-meta:${hash}`, { hash, size: bytes.length, refCount: 3, mimeType: 'application/json' })

    // Pre-adoption: _cas/ has neither → this is the "Blob metadata not found" state.
    expect(await storage.readRawObject(`_cas/blob-meta:${hash}`)).toBeNull()

    const r = await storage.adoptLegacyCowBlobs()
    expect(r).toMatchObject({ cowBlobs: 1, adopted: 1, alreadyPresent: 0, incomplete: 0 })

    // Post: _cas/ has both, metadata verbatim (refCount preserved), bytes intact.
    const casMeta = await storage.readRawObject(`_cas/blob-meta:${hash}`)
    expect(casMeta).toMatchObject({ hash, refCount: 3, size: bytes.length })
    const casBlob = await storage.readRawObject(`_cas/blob:${hash}`)
    expect(Buffer.from(casBlob.data, 'base64').toString()).toBe('the homepage payload')

    // Idempotent: a second run adopts nothing, counts the pair present.
    expect(await storage.adoptLegacyCowBlobs()).toMatchObject({ adopted: 0, alreadyPresent: 1 })
    // Non-destructive: the _cow/ original is still there (rollback stays possible).
    expect(await storage.readRawObject(`_cow/blob:${hash}`)).not.toBeNull()

    // Incomplete: bytes without metadata are reported, not half-adopted.
    await storage.writeRawObject(`_cow/blob:${'b'.repeat(64)}`, { _binary: true, data: 'x' })
    const r3 = await storage.adoptLegacyCowBlobs()
    expect(r3.incomplete).toBe(1)
    expect(await storage.readRawObject(`_cas/blob:${'b'.repeat(64)}`)).toBeNull() // not adopted
    await teardownRawStorage(storage)
  })

  it('a brain that upgraded into the stranded state self-heals on next open — the VFS page reads again', async () => {
    const dir = mkTmp()

    // 1. Author a VFS page (its content blob lands in _cas/).
    let brain: any = new Brainy({ storage: { type: 'filesystem', path: dir }, plugins: [], silent: true })
    await brain.init()
    await brain.vfs.writeFile('/pages/homepage/page.json', '{"title":"Homepage"}')
    expect((await brain.vfs.readFile('/pages/homepage/page.json')).toString()).toContain('Homepage')
    await brain.close()

    // 2. Reproduce the post-7→8 stranding: the blob is now only under _cow/.
    const moved = await strandCasBlobsIntoCow(dir)
    expect(moved).toBeGreaterThan(0)

    // 3. Reopen: the on-open auto-adoption heals it with no operator action.
    brain = new Brainy({ storage: { type: 'filesystem', path: dir }, plugins: [], silent: true })
    await brain.init()
    const healed = await brain.vfs.readFile('/pages/homepage/page.json')
    expect(healed.toString()).toContain('Homepage')

    // A subsequent open records the adoption marker and does not re-scan-heal.
    await brain.close()
    brain = new Brainy({ storage: { type: 'filesystem', path: dir }, plugins: [], silent: true })
    await brain.init()
    expect((await brain.vfs.readFile('/pages/homepage/page.json')).toString()).toContain('Homepage')
    await brain.close()
  })

  it('brain.vfs.adoptOrphanedBlobs() is the explicit recovery equivalent', async () => {
    const dir = mkTmp()
    const body = JSON.stringify({ page: 'about', body: 'x'.repeat(400) })
    let brain: any = new Brainy({ storage: { type: 'filesystem', path: dir }, plugins: [], silent: true })
    await brain.init()
    await brain.vfs.writeFile('/about.json', body)
    await brain.close()
    await strandCasBlobsIntoCow(dir)

    // The explicit API self-initializes (like other VFS methods); init's
    // auto-heal adopts, so the explicit call sees the pair present. Either way
    // it reports the _cow/ blobs it accounted for, and the read is healed.
    brain = new Brainy({ storage: { type: 'filesystem', path: dir }, plugins: [], silent: true })
    const r = await brain.vfs.adoptOrphanedBlobs()
    expect(r.cowBlobs).toBeGreaterThan(0)
    expect(r.adopted + r.alreadyPresent).toBe(r.cowBlobs)
    expect((await brain.vfs.readFile('/about.json')).toString()).toContain('about')
    await brain.close()
  })
})
