import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'

const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-colorlist-failure.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyColorList failure check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-colorlist-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyColorList') fail('baseline is invalid')
const baseline = [[0.06666667, 0.13333334, 0.200000003, 0.266666681], [1, 1, 1, 1]]
if (report.object?.name !== 'ColorListProbe' || report.object.typeId !== 'App::FeatureTest' || report.property?.name !== 'ColourList' || report.property.typeId !== 'App::PropertyColorList' || !isDeepStrictEqual(report.initial, baseline)) fail('native fixture identity changed')

const expectedCoercions = new Map([
  ['boolean-scalar', { requested: 'True', after: [[0, 0, 0, 0.003921569]] }],
  ['bytes-sequence', { requested: "b'\\x01\\xff'", after: [[0, 0, 0, 0.003921569], [0, 0, 0, 1]] }],
  ['bare-float-tuple', { requested: '(0.1, 0.2, 0.3, 0.4)', after: [[0.100000001, 0.200000003, 0.300000012, 0.400000006]] }],
  ['bare-integer-tuple', { requested: '(255, 128, 0, 64)', after: [[1, 0.501960814, 0, 0.250980407]] }],
  ['tuple-packed-sequence', { requested: '(287454020, 4294967295)', after: baseline }],
  ['empty-dictionary', { requested: '{}', after: [] }],
])
if (report.coercions?.length !== expectedCoercions.size) fail('coercion case count changed')
for (const entry of report.coercions) {
  const expected = expectedCoercions.get(entry.id)
  if (!expected || entry.requested !== expected.requested || entry.accepted !== true || entry.exception !== null || !isDeepStrictEqual(entry.before, baseline) || !isDeepStrictEqual(entry.after, expected.after) || entry.valuePreserved !== isDeepStrictEqual(baseline, expected.after) || entry.objectsPreserved !== true || !isDeepStrictEqual(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} accepted coercion changed`)
}

const expectedFailures = new Map([
  ['none', { requested: 'None', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not NoneType' }],
  ['bare-float-list', { requested: '[0.1, 0.2, 0.3, 0.4]', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not float' }],
  ['bare-mixed-tuple', { requested: '(0.1, 2, 0.3)', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not float' }],
  ['nested-short-tuple', { requested: '[(0.1, 0.2)]', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not tuple' }],
  ['mixed-valid-none', { requested: '[(0.1, 0.2, 0.3, 0.4), None]', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not NoneType' }],
  ['scalar-float', { requested: '0.5', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not float' }],
  ['string-sequence', { requested: "'rgb'", type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not str' }],
  ['nested-list', { requested: '[[0.1, 0.2, 0.3, 0.4]]', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not list' }],
  ['dictionary-string-key', { requested: "{'0': 287454020}", type: 'TypeError', message: 'expect key type to be integer' }],
  ['dictionary-high-index', { requested: '{3: 287454020}', type: 'ValueError', message: 'index out of bound' }],
  ['dictionary-negative-index', { requested: '{-2: 287454020}', type: 'ValueError', message: 'index out of bound' }],
  ['dictionary-partial-invalid', { requested: '{0: 16909060, 1: None}', type: 'TypeError', message: 'type must be integer or tuple of float or tuple integer, not NoneType', after: [[0.003921569, 0.007843138, 0.011764706, 0.015686275], [1, 1, 1, 1]] }],
])
if (report.failures?.length !== expectedFailures.size) fail('failure case count changed')
for (const entry of report.failures) {
  const expected = expectedFailures.get(entry.id)
  const expectedAfter = expected?.after ?? baseline
  if (!expected || entry.requested !== expected.requested || entry.exception?.type !== expected.type || entry.exception.message !== expected.message || !isDeepStrictEqual(entry.before, baseline) || !isDeepStrictEqual(entry.after, expectedAfter) || entry.valuePreserved !== isDeepStrictEqual(baseline, expectedAfter) || entry.objectsPreserved !== true || entry.polluted !== false || !isDeepStrictEqual(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} failure behavior changed`)
}
if (report.failures.filter(({ valuePreserved }) => !valuePreserved).map(({ id }) => id).join(',') !== 'dictionary-partial-invalid') fail('native partial-mutation boundary changed')

const disabled = report.disabled
if (disabled?.classification !== 'editor-readonly-python-mutable-until-immutable' || !isDeepStrictEqual(disabled.before, baseline) || !isDeepStrictEqual(disabled.editorMode, ['ReadOnly']) || !isDeepStrictEqual(disabled.editorReadOnlyRequested, [[0.2, 0.3, 0.4, 0.5]]) || disabled.editorReadOnlyException !== null || disabled.pythonBypassesEditorReadOnly !== true || !isDeepStrictEqual(disabled.editorReadOnlyAfter, [[0.200000003, 0.300000012, 0.400000006, 0.5]])) fail('editor ReadOnly boundary changed')
if (!isDeepStrictEqual(disabled.immutableStatus, ['Immutable', 'ReadOnly']) || !isDeepStrictEqual(disabled.immutableRequested, [[0.6, 0.7, 0.8, 0.9]]) || disabled.immutableException?.type !== 'AttributeError' || disabled.immutableException.message !== "Object attribute 'ColourList' is read-only" || !isDeepStrictEqual(disabled.immutableAfter, baseline) || disabled.immutableValuePreserved !== true || disabled.objectsPreserved !== true || !isDeepStrictEqual(disabled.restoredStatus, []) || !isDeepStrictEqual(disabled.restoredEditorMode, [])) fail('Immutable rejection or status restoration changed')

const transaction = report.transaction
const edited = [[0.75, 0.5, 0.25, 0.125], baseline[0], [0, 1, 0, 0.501960814]]
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-colorlist-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort.id !== 0 || transaction.restored !== true || transaction.objectsRestored !== true || transaction.recoveryRecomputeResult !== true) fail('transaction lifecycle changed')
if (!isDeepStrictEqual(transaction.before, baseline) || !isDeepStrictEqual(transaction.edited, edited) || !isDeepStrictEqual(transaction.afterAbort, baseline) || !isDeepStrictEqual(transaction.afterRecompute, baseline) || !isDeepStrictEqual(transaction.stateAfterEdit, { state: ['Touched'], statusString: 'Touched' }) || !isDeepStrictEqual(transaction.stateAfterAbort, { state: ['Touched'], statusString: 'Touched' }) || !isDeepStrictEqual(transaction.stateAfterRecompute, { state: ['Up-to-date'], statusString: 'Valid' }) || !isDeepStrictEqual(transaction.objectsBefore, transaction.objectsAfter)) fail('transaction abort did not restore value and state')
if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter' || report.cancellationBoundary.reason !== 'no-native-cancel-hook' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is not explicit')
if (report.documentIntegrity?.objectsPreserved !== true || !isDeepStrictEqual(report.documentIntegrity.initialObjects, report.documentIntegrity.finalObjects)) fail('failure cases polluted the document object set')
console.log(JSON.stringify({ status: 'freecad-property-colorlist-failure-pass', acceptedCoercions: report.coercions.map(({ id, after }) => ({ id, after })), rejectedCases: report.failures.map(({ id, exception, valuePreserved }) => ({ id, exception, valuePreserved })), partialMutation: report.failures.find(({ id }) => id === 'dictionary-partial-invalid'), disabled: { pythonBypassesEditorReadOnly: disabled.pythonBypassesEditorReadOnly, immutableException: disabled.immutableException }, transactionRestored: transaction.restored, cancellationBoundary: report.cancellationBoundary }, null, 2))
