const CASES = [
  ...['fuse', 'cut', 'common'].flatMap((operation) => [1, 2, 3].map((variant) => [`boolean-${operation}-${variant}`, 'boolean'])),
  ...['plain', 'midplane', 'reverse', 'taper', 'twoside', 'pocket', 'pocket-through', 'pocket-midplane', 'pocket-twoside', 'pocket-taper', 'pocket-up-to-face'].map((mode) => [`partdesign-${mode}`, 'partdesign']),
  ...[1, 2, 3, 4, 5, 6].map((variant) => [`composite-${String(variant).padStart(2, '0')}`, 'composite-boolean']),
  ...['fillet', 'chamfer'].flatMap((operation) => [1, 2].map((variant) => [`dressup-${operation}-${variant}`, 'dress-up'])),
]

export const expectedCompositeMutationCases = new Map(CASES)
export const expectedCompositeMutationCategoryCases = {
  boolean: 9,
  partdesign: 11,
  'composite-boolean': 6,
  'dress-up': 4,
}

const fail = (message) => { throw new Error(`FreeCAD composite mutation evidence: ${message}`) }
const canonical = (value) => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonical(entry)])) : value
const same = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right))
const sha256 = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value)
const stageFingerprint = (stage) => [stage?.shape?.geometryDigest, stage?.relationDigest, stage?.semanticNameDigest]
const geometryFingerprint = (stage) => stage?.shape?.geometryDigest
const namingFingerprint = (stage) => [stage?.relationDigest, stage?.semanticNameDigest]

const validatePhaseRecord = (fixture, nominalStage, ordinal, phase, record) => {
  const expectedKey = `${fixture.id}:${String(ordinal).padStart(3, '0')}:${nominalStage.name}`
  if (record?.key !== expectedKey || record.ordinal !== ordinal || record.name !== nominalStage.name || record.typeId !== nominalStage.typeId) fail(`${fixture.id}/${phase}/${ordinal} changed its locked stage identity.`)
  if (record.shape?.valid !== true || !sha256(record.shape?.brepSha256) || !sha256(record.shape?.geometryDigest) || !sha256(record.relationDigest) || !sha256(record.semanticNameDigest)) fail(`${fixture.id}/${phase}/${nominalStage.name} lacks valid geometry or naming digests.`)
}

export const validateCompositeMutationEvidence = (report) => {
  if (report?.mutationContractVersion !== 1 || !Array.isArray(report.cases) || report.cases.length !== CASES.length) fail('the report does not declare the locked 30-case mutation contract.')
  if (!same(report.cases.map(({ id }) => id), CASES.map(([id]) => id))) fail('case identities or ordering differ from the locked constructor inventory.')

  const categoryCases = Object.fromEntries(Object.keys(expectedCompositeMutationCategoryCases).map((category) => [category, 0]))
  let mutationCases = 0
  let mutationPassed = 0
  let mutationStageRecords = 0
  let mutationPhaseStageRecords = 0
  let mutationFinalPropagationFailures = 0
  let mutationStageRestoreFailures = 0
  let mutationNamingRestoreDriftCases = 0
  let mutationNamingRestoreDriftStages = 0
  const cases = []

  for (const fixture of report.cases) {
    const expectedCategory = expectedCompositeMutationCases.get(fixture.id)
    if (fixture.category !== expectedCategory || fixture.status !== 'pass') fail(`${fixture.id} is not a passing ${expectedCategory} case.`)
    categoryCases[fixture.category] += 1
    const mutation = fixture.mutation
    const contract = mutation?.contract
    const metrics = mutation?.metrics
    const phases = mutation?.phases
    if (mutation?.schemaVersion !== 1 || mutation.category !== fixture.category || mutation.status !== 'pass' || !contract || !metrics || !phases) fail(`${fixture.id} has no passing mutation transaction.`)
    if (contract.nativeEditable !== true || !Array.isArray(contract.editorModes) || contract.editorModes.includes('ReadOnly') || contract.editorModes.includes('Hidden') || !/^App::Property/.test(contract.propertyType || '') || typeof contract.propertyPath !== 'string' || !contract.propertyPath) fail(`${fixture.id} did not mutate a native editable FreeCAD property.`)
    if (contract.finalObject !== fixture.finalObject || contract.requiresFinalPropagation !== true || contract.requiresAllStageRestore !== true) fail(`${fixture.id} changed its propagation or restoration contract.`)
    if (!Number.isInteger(contract.targetStageOrdinal) || !Number.isInteger(contract.finalStageOrdinal) || contract.targetStageOrdinal < 0 || contract.finalStageOrdinal < 0 || contract.targetStageOrdinal >= fixture.stages.length || contract.finalStageOrdinal >= fixture.stages.length) fail(`${fixture.id} has invalid target/final stage ordinals.`)
    if (fixture.stages[contract.targetStageOrdinal]?.name !== contract.targetObject || fixture.stages[contract.targetStageOrdinal]?.typeId !== contract.targetTypeId || fixture.stages[contract.finalStageOrdinal]?.name !== fixture.finalObject) fail(`${fixture.id} mutation targets are not bound to captured stages.`)
    if (same(mutation.values?.before, mutation.values?.edited) || !same(mutation.values?.before, mutation.values?.restored)) fail(`${fixture.id} property edit or restoration readback is invalid.`)
    if (!['before', 'edited', 'restored'].every((phase) => Array.isArray(phases[phase]) && phases[phase].length === fixture.stages.length)) fail(`${fixture.id} lacks a full three-phase stage matrix.`)

    for (let ordinal = 0; ordinal < fixture.stages.length; ordinal += 1) {
      const nominal = fixture.stages[ordinal]
      for (const phase of ['before', 'edited', 'restored']) validatePhaseRecord(fixture, nominal, ordinal, phase, phases[phase][ordinal])
      if (!same(stageFingerprint(phases.restored[ordinal]), stageFingerprint(nominal))) fail(`${fixture.id}/${nominal.name} recovered state differs from the saved nominal stage.`)
    }

    const changedStageOrdinals = fixture.stages.flatMap((_stage, ordinal) => geometryFingerprint(phases.before[ordinal]) === geometryFingerprint(phases.edited[ordinal]) ? [] : [ordinal])
    const restorationDriftOrdinals = fixture.stages.flatMap((_stage, ordinal) => geometryFingerprint(phases.before[ordinal]) === geometryFingerprint(phases.restored[ordinal]) ? [] : [ordinal])
    const namingChangedStageOrdinals = fixture.stages.flatMap((_stage, ordinal) => same(namingFingerprint(phases.before[ordinal]), namingFingerprint(phases.edited[ordinal])) ? [] : [ordinal])
    const namingRestorationDriftOrdinals = fixture.stages.flatMap((_stage, ordinal) => same(namingFingerprint(phases.before[ordinal]), namingFingerprint(phases.restored[ordinal])) ? [] : [ordinal])
    if (!same(metrics.changedStageOrdinals, changedStageOrdinals) || metrics.changedStages !== changedStageOrdinals.length || !changedStageOrdinals.includes(contract.finalStageOrdinal)) fail(`${fixture.id} did not propagate its native edit to the final object.`)
    if (!same(metrics.restorationDriftOrdinals, restorationDriftOrdinals) || metrics.restorationDriftStages !== restorationDriftOrdinals.length || restorationDriftOrdinals.length !== 0) fail(`${fixture.id} did not restore every stage exactly.`)
    if (!same(metrics.namingChangedStageOrdinals, namingChangedStageOrdinals) || metrics.namingChangedStages !== namingChangedStageOrdinals.length || !same(metrics.namingRestorationDriftOrdinals, namingRestorationDriftOrdinals) || metrics.namingRestorationDriftStages !== namingRestorationDriftOrdinals.length || metrics.allStageNamingRestored !== (namingRestorationDriftOrdinals.length === 0)) fail(`${fixture.id} naming edit/restoration metrics do not match its native phase evidence.`)
    if (metrics.propertyChanged !== true || metrics.finalShapeChanged !== true || metrics.finalBrepChanged !== true || metrics.propertyRestored !== true || metrics.allStagesRestored !== true || metrics.allStageGeometryRestored !== true || metrics.editedShapesValid !== true) fail(`${fixture.id} mutation acceptance metrics are incomplete.`)
    if (metrics.stageRecords !== fixture.stages.length || metrics.phaseStageRecords !== fixture.stages.length * 3) fail(`${fixture.id} mutation stage record counts are inconsistent.`)

    mutationCases += 1
    mutationPassed += 1
    mutationStageRecords += metrics.stageRecords
    mutationPhaseStageRecords += metrics.phaseStageRecords
    mutationFinalPropagationFailures += metrics.finalShapeChanged === true ? 0 : 1
    mutationStageRestoreFailures += metrics.restorationDriftStages
    mutationNamingRestoreDriftCases += metrics.namingRestorationDriftStages > 0 ? 1 : 0
    mutationNamingRestoreDriftStages += metrics.namingRestorationDriftStages
    cases.push({ id: fixture.id, category: fixture.category, stages: fixture.stages.length, changedStages: metrics.changedStages, restoredStages: metrics.stageRecords, namingRestoreDriftStages: metrics.namingRestorationDriftStages })
  }

  const summary = { mutationCases, mutationPassed, mutationStageRecords, mutationPhaseStageRecords, mutationFinalPropagationFailures, mutationStageRestoreFailures, mutationNamingRestoreDriftCases, mutationNamingRestoreDriftStages, categoryCases }
  for (const [key, value] of Object.entries(summary)) {
    if (!same(report.summary?.[key], value)) fail(`summary ${key} differs from per-case evidence.`)
  }
  if (!same(categoryCases, expectedCompositeMutationCategoryCases) || mutationCases !== 30 || mutationPassed !== 30 || mutationStageRecords !== 219 || mutationPhaseStageRecords !== 657 || mutationFinalPropagationFailures !== 0 || mutationStageRestoreFailures !== 0) fail(`locked totals are incomplete: ${JSON.stringify(summary)}`)
  return { ...summary, cases }
}
