import assert from 'node:assert/strict'
import { writeFile } from 'node:fs/promises'
import { performance } from 'node:perf_hooks'
import { strToU8, zipSync } from 'fflate'
import { inspectFcstdArchive } from '../src/facade/fcstd'

type FuzzCase = {
  name: string
  archive: Uint8Array
  expected: 'accept' | 'reject'
  limits?: Parameters<typeof inspectFcstdArchive>[1]
}

let state = 0x5f3759df
const random = () => {
  state ^= state << 13
  state ^= state >>> 17
  state ^= state << 5
  return state >>> 0
}
const token = () => `Object_${random().toString(16).padStart(8, '0')}`
const archive = (documentXml: string | Uint8Array, guiXml?: string, extra: Record<string, Uint8Array> = {}) => zipSync({
  'Document.xml': typeof documentXml === 'string' ? strToU8(documentXml) : documentXml,
  ...(guiXml === undefined ? {} : { 'GuiDocument.xml': strToU8(guiXml) }),
  ...extra,
})

const createCase = (index: number): FuzzCase => {
  const name = token()
  switch (index % 13) {
    case 0: return { name: 'empty-document', archive: archive('<Document/>'), expected: 'accept' }
    case 1: return { name: 'proxy-object', archive: archive(`<Document SchemaVersion="4"><Objects><Object name="${name}" type="Vendor::Proxy"/></Objects><ObjectData><Object name="${name}"><Properties/></Object></ObjectData></Document>`), expected: 'accept' }
    case 2: return { name: 'malformed-close', archive: archive('<Document><Objects></Document>'), expected: 'reject' }
    case 3: return { name: 'wrong-document-root', archive: archive('<Objects/>'), expected: 'reject' }
    case 4: return { name: 'duplicate-object-id', archive: archive(`<Document><Objects><Object name="${name}" type="Part::Feature"/><Object name="${name}" type="Part::Feature"/></Objects></Document>`), expected: 'reject' }
    case 5: return { name: 'empty-object-id', archive: archive('<Document><Objects><Object name="" type="Part::Feature"/></Objects></Document>'), expected: 'reject' }
    case 6: return { name: 'depth-limit', archive: archive(`<Document>${'<Group>'.repeat(12)}${'</Group>'.repeat(12)}</Document>`), expected: 'reject', limits: { maxXmlDepth: 8 } }
    case 7: return { name: 'node-limit', archive: archive(`<Document>${Array.from({ length: 32 }, (_, item) => `<N${item}/>`).join('')}</Document>`), expected: 'reject', limits: { maxXmlNodes: 20 } }
    case 8: return { name: 'entity-declaration', archive: archive('<!DOCTYPE Document [<!ENTITY x "boom">]><Document>&x;</Document>'), expected: 'reject' }
    case 9: return { name: 'invalid-utf8', archive: archive(new Uint8Array([0x3c, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x3e, 0xc3, 0x28, 0x3c, 0x2f, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x3e])), expected: 'reject' }
    case 10: return { name: 'invalid-gui-root', archive: archive('<Document/>', '<Views/>'), expected: 'reject' }
    case 11: return { name: 'opaque-resource', archive: archive('<Document/>', '<GuiDocument/>', { [`Resources/${name}.bin`]: new Uint8Array([random() & 0xff, random() & 0xff]) }), expected: 'accept' }
    default: return { name: 'traversal-path', archive: archive('<Document/>', undefined, { [`../${name}.bin`]: new Uint8Array([1]) }), expected: 'reject' }
  }
}

const durations: number[] = []
const counts = { accepted: 0, rejected: 0, validCases: 0, invalidCases: 0 }
const categories = new Map<string, number>()
for (let index = 0; index < 1000; index += 1) {
  const fuzzCase = createCase(index)
  categories.set(fuzzCase.name, (categories.get(fuzzCase.name) ?? 0) + 1)
  const started = performance.now()
  let outcome: 'accept' | 'reject' = 'accept'
  let caught: unknown
  try {
    inspectFcstdArchive(fuzzCase.archive, fuzzCase.limits)
  } catch (error) {
    outcome = 'reject'
    caught = error
  }
  durations.push(performance.now() - started)
  if (outcome === 'accept') counts.accepted += 1
  else counts.rejected += 1
  if (fuzzCase.expected === 'accept') counts.validCases += 1
  else counts.invalidCases += 1
  assert.equal(outcome, fuzzCase.expected, `FCStd fuzz category ${fuzzCase.name} produced ${outcome}, expected ${fuzzCase.expected}.`)
  if (outcome === 'reject') assert.ok(caught instanceof Error, `FCStd fuzz category ${fuzzCase.name} rejected with a non-Error value.`)
}

durations.sort((left, right) => left - right)
const p95Ms = durations[Math.floor(durations.length * 0.95)]
assert.ok(p95Ms < 50, `FCStd parser fuzz p95 exceeded 50 ms: ${p95Ms.toFixed(3)} ms.`)
const report = {
  status: 'fcstd-parser-fuzz-pass',
  seed: '0x5f3759df',
  cases: durations.length,
  counts,
  categories: Object.fromEntries([...categories].sort(([left], [right]) => left.localeCompare(right))),
  timing: { p95Ms: Number(p95Ms.toFixed(3)), maxMs: Number(durations.at(-1)?.toFixed(3)) },
}
await writeFile(new URL('../config/qa04-fcstd-fuzz-verification.json', import.meta.url), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))
