/**
 * Process-exit sweep — no operation class may leave a ref'd timer behind.
 *
 * A consumer's clean-room verification of the close()-hang fix found the
 * add-only path clean but `relate()` leaving ~5 uncleared Timeouts — the fix
 * had covered a repro, not the bug class. This sweep turns the class off:
 * for EVERY op family (add / relate / graph find / vfs), assert that after
 * `close()` no NEW ref'd Timeout survives (unref'd timers are fine — they
 * never keep the host process alive; that is the standard for every
 * background-maintenance interval in brainy).
 *
 * Filesystem storage on purpose: it is the reported environment and the one
 * that arms the most timers (LSM flush/compaction, write buffers, watchers).
 */
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, NounType, VerbType } from '../../src/index.js'

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

const V = () => Array.from({ length: 384 }, () => Math.random())

/** Identity set of currently-ref'd Timeout handles (the only kind that can
 *  keep the event loop — and a bare consumer script — alive). */
function refdTimeouts(): Set<unknown> {
  const handles: any[] = (process as any)._getActiveHandles?.() ?? []
  return new Set(
    handles.filter(
      (h) =>
        h?.constructor?.name === 'Timeout' &&
        (typeof h.hasRef !== 'function' || h.hasRef())
    )
  )
}

/** Describe a leaked timer well enough to find its source. */
function describeTimer(h: any): string {
  const fn = h?._onTimeout
  return `Timeout(after=${h?._idleTimeout}ms, repeat=${!!h?._repeat}, fn=${
    fn?.name || 'anon'
  }: ${String(fn).slice(0, 120).replace(/\s+/g, ' ')})`
}

async function sweep(run: (brain: any) => Promise<void>): Promise<string[]> {
  const before = refdTimeouts()
  const brain: any = new Brainy({
    requireSubtype: false,
    storage: { type: 'filesystem', path: mkTmp() },
    plugins: [],
    silent: true
  })
  await brain.init()
  await run(brain)
  await brain.close()
  return [...refdTimeouts()].filter((h) => !before.has(h)).map(describeTimer)
}

describe('Process-exit sweep — close() leaves no ref’d timers, per op class', () => {
  it('add', async () => {
    const leaked = await sweep(async (brain) => {
      await brain.add({ data: 'solo', type: NounType.Concept, subtype: 's', vector: V() })
    })
    expect(leaked).toEqual([])
  })

  it('add + relate (the reported hang shape)', async () => {
    const leaked = await sweep(async (brain) => {
      const a = await brain.add({ data: 'a', type: NounType.Concept, subtype: 's', vector: V() })
      const b = await brain.add({ data: 'b', type: NounType.Concept, subtype: 's', vector: V() })
      await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 's' })
    })
    expect(leaked).toEqual([])
  })

  it('relate + graph find + related()', async () => {
    const leaked = await sweep(async (brain) => {
      const ids: string[] = []
      for (let i = 0; i < 6; i++) {
        ids.push(await brain.add({ data: `n${i}`, type: NounType.Concept, subtype: 's', vector: V() }))
      }
      for (let i = 0; i + 1 < ids.length; i++) {
        await brain.relate({ from: ids[i], to: ids[i + 1], type: VerbType.RelatedTo, subtype: 's' })
      }
      await brain.find({ connected: { from: ids[0], depth: 2 } })
      await brain.related({ node: ids[1] })
    })
    expect(leaked).toEqual([])
  })

  it('metadata find + update (write-buffer path)', async () => {
    const leaked = await sweep(async (brain) => {
      const id = await brain.add({
        data: 'meta', type: NounType.Concept, subtype: 's',
        metadata: { wave: 1 }, vector: V()
      })
      await brain.update({ id, metadata: { wave: 2 } })
      await brain.find({ where: { wave: { greaterThan: 1 } } })
    })
    expect(leaked).toEqual([])
  })

  it('vfs write/read/tree (VFS + PathResolver timers)', async () => {
    const leaked = await sweep(async (brain) => {
      await brain.vfs.writeFile('/notes/a.md', 'alpha')
      await brain.vfs.readFile('/notes/a.md')
      await brain.vfs.getTreeStructure('/', { maxDepth: 2 })
    })
    expect(leaked).toEqual([])
  })
})
