import assert from 'node:assert/strict'
import { writeFile } from 'node:fs/promises'
import { performance } from 'node:perf_hooks'
import { createSketch, solveSketch, type SketchConstraint, type SketchDiagnostic, type SketchGeometry, type SketchSolverStatus } from '../src/facade/sketcher'

type SolverFuzzCase = {
  name: string
  geometry: SketchGeometry[]
  constraints: SketchConstraint[]
  expectedStatus?: SketchSolverStatus
  expectedCode?: SketchDiagnostic['code']
}

let state = 0xa5a5f00d
const random = () => {
  state ^= state << 13
  state ^= state >>> 17
  state ^= state << 5
  return state >>> 0
}
const value = () => (random() % 100_000) / 1000
const line = (id: string, offset = value()): Extract<SketchGeometry, { type: 'line' }> => ({ id, type: 'line', start: { x: offset, y: value() }, end: { x: offset + 1 + value(), y: value() } })

const createCase = (index: number): SolverFuzzCase => {
  const first = line('first')
  const second = line('second')
  switch (index % 10) {
    case 0: return { name: 'horizontal', geometry: [first], constraints: [{ id: 'horizontal', type: 'horizontal', geometryId: 'first' }], expectedStatus: 'under-constrained' }
    case 1: return { name: 'parallel', geometry: [first, second], constraints: [{ id: 'parallel', type: 'parallel', firstGeometryId: 'first', secondGeometryId: 'second' }], expectedStatus: 'under-constrained' }
    case 2: return { name: 'unknown-geometry', geometry: [first], constraints: [{ id: 'missing', type: 'vertical', geometryId: 'absent' }], expectedStatus: 'invalid', expectedCode: 'UNKNOWN_GEOMETRY' }
    case 3: return { name: 'invalid-dimension', geometry: [first], constraints: [{ id: 'distance', type: 'distance', first: { geometryId: 'first', point: 'start' }, second: { geometryId: 'first', point: 'end' }, value: Number.NaN }], expectedStatus: 'invalid', expectedCode: 'INVALID_VALUE' }
    case 4: return { name: 'redundant', geometry: [first], constraints: [{ id: 'h1', type: 'horizontal', geometryId: 'first' }, { id: 'h2', type: 'horizontal', geometryId: 'first' }], expectedStatus: 'under-constrained', expectedCode: 'REDUNDANT_CONSTRAINT' }
    case 5: return { name: 'reference-distance', geometry: [first], constraints: [{ id: 'reference', type: 'distanceX', first: { geometryId: 'first', point: 'start' }, second: { geometryId: 'first', point: 'end' }, value: value(), driving: false }], expectedStatus: 'under-constrained' }
    case 6: return { name: 'line-circle-tangent', geometry: [first, { id: 'circle', type: 'circle', center: { x: value(), y: value() }, radius: 1 + value() }], constraints: [{ id: 'tangent', type: 'tangent', firstGeometryId: 'first', secondGeometryId: 'circle' }], expectedStatus: 'under-constrained' }
    case 7: return { name: 'unsupported-bspline-weight', geometry: [{ id: 'curve', type: 'bspline', degree: 2, controlPoints: [{ x: 0, y: 0 }, { x: 1, y: value() }, { x: 2, y: 0 }], weights: [1, 1, 1] }], constraints: [{ id: 'weight', type: 'weight', geometryId: 'curve', controlPointIndex: 1, value: 2 }], expectedStatus: 'invalid', expectedCode: 'UNSUPPORTED_CONSTRAINT' }
    case 8: return { name: 'unknown-point', geometry: [first], constraints: [{ id: 'bad-point', type: 'coincident', first: { geometryId: 'first', point: 'center' }, second: { geometryId: 'first', point: 'end' } }], expectedStatus: 'invalid', expectedCode: 'UNKNOWN_POINT' }
    default: return { name: 'blocked-conflict', geometry: [first], constraints: [{ id: 'block', type: 'block', geometryId: 'first' }, { id: 'horizontal', type: 'horizontal', geometryId: 'first' }], expectedStatus: 'conflicting', expectedCode: 'SOLVER_NOT_CONVERGED' }
  }
}

const durations: number[] = []
const categories = new Map<string, number>()
const statuses: Record<SketchSolverStatus, number> = { solved: 0, 'under-constrained': 0, conflicting: 0, invalid: 0 }
for (let index = 0; index < 2000; index += 1) {
  const fuzzCase = createCase(index)
  categories.set(fuzzCase.name, (categories.get(fuzzCase.name) ?? 0) + 1)
  const snapshot = createSketch(`solver-fuzz-${index}`, fuzzCase.geometry, fuzzCase.constraints)
  const before = JSON.stringify(snapshot)
  const options = { tolerance: 1e-6, maxIterations: random() % 32 + 1 }
  const started = performance.now()
  const first = solveSketch(snapshot, options)
  const second = solveSketch(snapshot, options)
  durations.push(performance.now() - started)
  statuses[first.status] += 1
  assert.equal(JSON.stringify(first), JSON.stringify(second), `Sketch solver fuzz category ${fuzzCase.name} is non-deterministic.`)
  assert.equal(JSON.stringify(snapshot), before, `Sketch solver fuzz category ${fuzzCase.name} mutated its input Snapshot.`)
  assert.equal(first.status, fuzzCase.expectedStatus, `Sketch solver fuzz category ${fuzzCase.name} returned ${first.status}.`)
  assert.ok(first.iterations <= options.maxIterations, `Sketch solver fuzz category ${fuzzCase.name} exceeded its iteration limit.`)
  assert.ok(!Number.isNaN(first.residual), `Sketch solver fuzz category ${fuzzCase.name} produced NaN residual.`)
  if (fuzzCase.expectedCode) assert.ok(first.diagnostics.some((diagnostic) => diagnostic.code === fuzzCase.expectedCode), `Sketch solver fuzz category ${fuzzCase.name} omitted ${fuzzCase.expectedCode}.`)
  if (first.status === 'conflicting') assert.ok(first.diagnostics.some((diagnostic) => diagnostic.code === 'SOLVER_NOT_CONVERGED'))
  if (first.status === 'invalid') assert.ok(first.diagnostics.some((diagnostic) => diagnostic.code !== 'REDUNDANT_CONSTRAINT'))
}

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