import assert from 'node:assert/strict'
import { EventEmitter } from 'node:events'

import { test } from 'vitest'

import {
  claimDecision,
  createBackendOutputTail,
  DEFAULT_OUTPUT_TAIL_LIMIT,
  isPidOnlyStartMarker,
  pidOnlyStartMarker,
  probeStartMarker,
  processStartMarker
} from './backend-claim'

// --- claimDecision: the #93608 policy ---------------------------------------

test('probe success claims with the full start marker (unchanged behavior)', () => {
  const decision = claimDecision(true, { ok: true, startMarker: 'linux:12345' })

  assert.deepEqual(decision, { action: 'claim', startMarker: 'linux:12345' })
})

test('probe success claims even when the child already exited (ownership records the incarnation)', () => {
  // The claim itself must not invent a failure: the exit handler owns cleanup.
  const decision = claimDecision(false, { ok: true, startMarker: 'win:99' })

  assert.deepEqual(decision, { action: 'claim', startMarker: 'win:99' })
})

test('probe failure on a LIVE child degrades to PID-only identity — never kills a healthy backend (#93608)', () => {
  const decision = claimDecision(true, { ok: false, reason: 'powershell.exe timed out after 30000ms' })

  assert.equal(decision.action, 'degrade')
  assert.match((decision as { reason: string }).reason, /timed out/)
})

test('probe failure on a DEAD child fails closed so the caller can attach the stderr tail', () => {
  const decision = claimDecision(false, { ok: false, reason: 'Get-Process: no process with ID 4242' })

  assert.equal(decision.action, 'fail')
  assert.match((decision as { reason: string }).reason, /4242/)
})

// --- probeStartMarker: throw → value ----------------------------------------

test('probeStartMarker converts a probe throw into { ok: false, reason }', async () => {
  const probe = await probeStartMarker(4242, async () => {
    throw new Error('PowerShell 5.1 cold start exceeded budget')
  })

  assert.deepEqual(probe, { ok: false, reason: 'PowerShell 5.1 cold start exceeded budget' })
})

test('probeStartMarker passes a successful marker through', async () => {
  const probe = await probeStartMarker(4242, async pid => `linux:${pid}`)

  assert.deepEqual(probe, { ok: true, startMarker: 'linux:4242' })
})

// --- real probe: drives the actual OS helper (PowerShell on the Windows lane) ---

test('processStartMarker resolves a real marker for the current process', async () => {
  const marker = await processStartMarker(process.pid)

  assert.match(marker, /^(linux|win|winms|ps):.+/)
})

test('a missing PID is classified as ESRCH so reapOrphans can drop the record', async () => {
  // Largest PIDs are bounded well below this on every supported platform.
  // Windows Get-Process / macOS `ps -p` used to surface exit code 1, which
  // the identity matchers treated as "unknown" and kept forever. The native
  // gate throws ESRCH — the errno those catch blocks already map to gone.
  await assert.rejects(processStartMarker(2 ** 30 + 12345), (error: NodeJS.ErrnoException) => error?.code === 'ESRCH')
})

// --- PID-only marker helpers --------------------------------------------------

test('pidOnlyStartMarker round-trips through isPidOnlyStartMarker', () => {
  const marker = pidOnlyStartMarker(4242)

  assert.equal(marker, 'pid-only:4242')
  assert.equal(isPidOnlyStartMarker(marker), true)
  assert.equal(isPidOnlyStartMarker('linux:12345'), false)
  assert.equal(isPidOnlyStartMarker(undefined), false)
})

// --- output tail ring buffer ----------------------------------------------------

test('output tail keeps only the most recent bytes once past the limit', () => {
  const tail = createBackendOutputTail(16)

  tail.append('0123456789')
  tail.append('abcdefghij')

  assert.equal(tail.text(), '456789abcdefghij')
  assert.equal(tail.text().length, 16)
})

test('output tail default limit is ~8KB', () => {
  const tail = createBackendOutputTail()

  tail.append('x'.repeat(DEFAULT_OUTPUT_TAIL_LIMIT + 500))

  assert.equal(tail.text().length, DEFAULT_OUTPUT_TAIL_LIMIT)
  assert.equal(DEFAULT_OUTPUT_TAIL_LIMIT, 8192)
})

test('output tail interleaves stdout and stderr attached from spawn time', () => {
  const child = { stderr: new EventEmitter(), stdout: new EventEmitter() }
  const tail = createBackendOutputTail(64)

  tail.attach(child)
  child.stdout.emit('data', Buffer.from('booting\n'))
  child.stderr.emit('data', Buffer.from("ModuleNotFoundError: No module named 'hermes_cli'\n"))

  assert.match(tail.text(), /booting/)
  assert.match(tail.text(), /ModuleNotFoundError/)
})

test('describe() is empty when nothing was captured, formatted when output exists', () => {
  const tail = createBackendOutputTail(64)

  assert.equal(tail.describe(), '')

  tail.append('Traceback (most recent call last):\n')
  assert.match(tail.describe(), /^\nRecent backend output:\nTraceback/)
})

test('attach tolerates a child with missing stdio streams', () => {
  const tail = createBackendOutputTail(64)

  tail.attach({ stderr: null, stdout: null })
  assert.equal(tail.text(), '')
})
