import fs from 'node:fs'

const read = p => JSON.parse(fs.readFileSync(p, 'utf8'))
const scenarios = read('contracts/testing/kbx.test-scenarios.json')
const fixtures = read('contracts/testing/kbx.test-fixtures.json')
const screens = read('generated/screen-manifest.json')
const apis = read('generated/api-manifest.json')
const permissions = read('generated/permission-manifest.json')
const problems = new Set(read('generated/problem-manifest.json').types ?? [])
const screenIds = new Set(screens.map ? screens.map(x=>x.id) : (screens.screens ?? []).map(x=>x.id))
const apiMap = new Map((apis.operations ?? []).map(x => [x.id, x]))
const permissionIds = new Set((permissions.permissions ?? []).map(x => x.id))
const fixtureSetIds = new Set((fixtures.fixtureSets ?? []).map(x => x.id))
const fixtureRefs = new Set(Object.keys(fixtures.refs ?? {}))
const errors = []

if (!fixtures.syntheticOnly || !scenarios.principles?.syntheticFixturesOnly || !scenarios.principles?.noProductionData)
  errors.push('test contracts must declare synthetic-only/no-production-data policy')
if (fixtures.fixedReferenceClock !== scenarios.principles?.fixedReferenceClock)
  errors.push('scenario and fixture reference clocks differ')

const ids = new Set()
for (const s of scenarios.scenarios ?? []) {
  if (ids.has(s.id)) errors.push(`duplicate scenario id: ${s.id}`)
  ids.add(s.id)
  if (!/^scenario\.[a-z0-9.-]+$/.test(s.id)) errors.push(`invalid scenario id: ${s.id}`)
  if (!screenIds.has(s.screenId)) errors.push(`${s.id}: unknown screenId ${s.screenId}`)
  if (!['e2e','integration','contract'].includes(s.kind)) errors.push(`${s.id}: invalid kind ${s.kind}`)
  if (!['database-reset','tenant-reset','ui-only'].includes(s.isolation)) errors.push(`${s.id}: invalid isolation ${s.isolation}`)
  if (!(s.assertions?.length > 0)) errors.push(`${s.id}: assertions required`)
  if (!(s.evidence?.length > 0)) errors.push(`${s.id}: evidence required`)
  for (const setId of s.fixtureSets ?? []) if (!fixtureSetIds.has(setId)) errors.push(`${s.id}: unknown fixture set ${setId}`)
  for (const p of s.requiredPermissions ?? []) if (!permissionIds.has(p)) errors.push(`${s.id}: unknown permission ${p}`)
  for (const opId of s.apiOperations ?? []) {
    const op = apiMap.get(opId)
    if (!op) { errors.push(`${s.id}: unknown api operation ${opId}`); continue }
    if (!(s.steps ?? []).some(step => step.operationId === opId)) errors.push(`${s.id}: API ${opId} declared but no step uses it`)
    if (op.idempotency === 'required') {
      const uses = (s.steps ?? []).filter(step => step.operationId === opId)
      if (!uses.some(step => typeof step.idempotencyKeyRef === 'string')) errors.push(`${s.id}: required-idempotency API ${opId} needs idempotencyKeyRef`)
    }
  }
  for (const step of s.steps ?? []) {
    if (step.operationId && !apiMap.has(step.operationId)) errors.push(`${s.id}/${step.id}: unknown step operation ${step.operationId}`)
    if (step.fixtureRef && !fixtureRefs.has(step.fixtureRef)) errors.push(`${s.id}/${step.id}: unknown fixtureRef ${step.fixtureRef}`)
    if (step.idempotencyKeyRef && !fixtureRefs.has(step.idempotencyKeyRef)) errors.push(`${s.id}/${step.id}: unknown idempotencyKeyRef ${step.idempotencyKeyRef}`)
    if (step.problemType && !problems.has(step.problemType)) errors.push(`${s.id}/${step.id}: unknown problemType ${step.problemType}`)
    const raw = JSON.stringify(step)
    if (/https?:\/\//i.test(raw) || /\/api\//i.test(raw)) errors.push(`${s.id}/${step.id}: raw URL/API route is forbidden; use operationId`)
  }
}

for (const set of fixtures.fixtureSets ?? []) {
  for (const ref of set.refs ?? []) if (!fixtureRefs.has(ref)) errors.push(`${set.id}: unknown ref ${ref}`)
}

// Simple synthetic-data guard: real-looking Korean mobile numbers must use 0000 exchange in canonical fixtures.
const fixtureText = JSON.stringify(fixtures)
for (const m of fixtureText.matchAll(/010-(\d{4})-(\d{4})/g)) if (m[1] !== '0000') errors.push(`fixture contains non-canonical phone pattern: ${m[0]}`)
for (const [ref, value] of Object.entries(fixtures.refs ?? {})) {
  const serialized = JSON.stringify(value)
  if (/\b\d{13}\b/.test(serialized)) errors.push(`${ref}: production-like 13-digit barcode forbidden in canonical fixture`)
  if (value.kind === 'customer' && !String(value.code ?? '').startsWith('TEST-')) errors.push(`${ref}: customer code must start TEST-`)
  if (value.kind === 'order' && !String(value.orderNo ?? '').startsWith('TEST-')) errors.push(`${ref}: orderNo must start TEST-`)
}

const golden = ['OMS-ORD-001','OMS-ORD-002','OMS-ORD-003','WMS-PICK-001','COMMON-OPS-001','COMMON-REC-001','ERP-INV-MOVE-001','ERP-PRICE-001','ERP-INV-001','ERP-MST-ITEM-001']
for (const screenId of golden) if (!(scenarios.scenarios ?? []).some(s => s.screenId === screenId)) errors.push(`canonical scenario missing for ${screenId}`)

const seed = fs.readFileSync('tests/fixtures/postgres/20-seed-v18.sql','utf8')
const reset = fs.readFileSync('tests/fixtures/postgres/10-reset-v18.sql','utf8')
for (const [name,text] of [['seed',seed],['reset',reset]]) {
  if (!text.includes('KBX_SCENARIO_TEST_ONLY')) errors.push(`${name} SQL missing environment guard`)
  if (/\btruncate\b/i.test(text)) errors.push(`${name} SQL must not truncate arbitrary host data`)
}

const generated = read('generated/test-scenario-manifest.json')
if (generated.scenarios.length !== scenarios.scenarios.length) errors.push('generated scenario manifest count mismatch')
const generatedFixtures = read('generated/test-fixture-manifest.json')
if (!generatedFixtures.syntheticOnly) errors.push('generated fixture manifest lost syntheticOnly')

const tsGenerated = fs.readFileSync('packages/kbx-contracts/src/generated/testScenarioCatalog.ts','utf8')
const csGenerated = fs.readFileSync('backend/Shared/Testing/Generated/KbxTestScenarioCatalog.g.cs','utf8')
if (!tsGenerated.includes(generated.sourceSha256) || !csGenerated.includes(generated.sourceSha256)) errors.push('scenario source SHA parity missing in TS/C# generated catalogs')
if (!tsGenerated.includes(generated.fixtureSourceSha256) || !csGenerated.includes(generated.fixtureSourceSha256)) errors.push('fixture source SHA parity missing in TS/C# generated catalogs')

if (errors.length) {
  console.error('test governance FAIL')
  for (const e of errors) console.error(`- ${e}`)
  process.exit(1)
}
console.log(`test governance PASS: scenarios=${scenarios.scenarios.length}, fixtureSets=${fixtures.fixtureSets.length}, refs=${fixtureRefs.size}, goldenCoverage=${golden.length}/${golden.length}`)
