import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'

const root = resolve(new URL('..', import.meta.url).pathname)
const runtimePath = resolve(root, '.cache/freecad/reference-desktop.json')
const reportPath = resolve(root, 'config/freecad-property-colorlist-inventory.json')
const fail = (message) => { throw new Error(`FreeCAD PropertyColorList inventory check failed: ${message}`) }
const [runtimeContent, reportContent] = await Promise.all([readFile(runtimePath), readFile(reportPath)])
const runtime = JSON.parse(runtimeContent)
const report = JSON.parse(reportContent)

if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyColorList' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1')
if (report.recordCount !== 2 || !Array.isArray(report.records) || report.records.length !== 2) fail('record count is not exactly two')
if (report.provenance?.runtime?.path !== '.cache/freecad/reference-desktop.json' || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale')
const storage = report.storage
if (storage?.xmlElement !== 'ColorList' || storage.fileAttribute !== 'file' || storage.nonEmptyExternalResourceName !== 'property-name' || storage.emptyResourceName !== '' || storage.externalBinary?.byteOrder !== 'little-endian' || storage.externalBinary.count !== 'uint32' || storage.externalBinary.element !== 'uint32-packed-0xRRGGBBAA' || storage.legacyBeforeVersion !== '1.1' || storage.legacyConversion !== 'invert-alpha-channel') fail('native storage contract is incomplete')

for (const objectTypeId of ['App::FeatureTest', 'App::FeatureTestException']) {
  const record = report.records.find((candidate) => candidate.objectTypeId === objectTypeId && candidate.propertyName === 'ColourList')
  if (!record || record.objectAvailable !== true || record.probeStatus !== 'available' || record.group !== '' || record.status?.length !== 0 || JSON.stringify(record.defaultValue) !== '[[0,0,0,1]]') fail(`native inventory changed for ${objectTypeId}.ColourList`)
  const model = record.valueModel
  if (model?.kind !== 'ordered-rgba-color-list' || model.elementRepresentation !== 'four-channel-float-tuple' || model.packedElementRepresentation !== '0xRRGGBBAA' || model.alphaSemantics !== '1-opaque-0-transparent' || model.writable !== true) fail(`value model changed for ${objectTypeId}.ColourList`)
  if (record.inputs?.collection?.join(',') !== 'sequence,iterable,single-tuple-color-value,single-non-sequence-color-value,integer-index-dictionary' || JSON.stringify(record.inputs.dictionaryAppendIndexes) !== '[-1,"current-size"]' || record.inputs.elementSetters?.join(',') !== 'float-tuple-3-or-4,integer-tuple-3-or-4,packed-integer-0xRRGGBBAA') fail(`setter inputs changed for ${objectTypeId}.ColourList`)
  if (!Array.isArray(record.dependencies) || record.dependencies.length !== 0 || record.applicability?.requiredObjectTypeId !== objectTypeId || record.applicability.source !== 'Document.supportedTypes runtime inventory' || record.applicability.shapeRequired !== false) fail(`dependencies or applicability changed for ${objectTypeId}.ColourList`)
}

const nativeMatches = []
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyColorList') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultValue: property.default })
if (nativeMatches.length !== 2) fail('locked runtime oracle no longer contains two PropertyColorList records')
for (const locked of report.provenance?.sources ?? []) {
  const content = await readFile(resolve(root, locked?.path ?? ''))
  if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`source provenance is stale for ${locked.path}`)
}
const [propertySource, propertyBase, streamSource, colorHeader] = await Promise.all([
  readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyStandard.cpp'), 'utf8'),
  readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/Property.cpp'), 'utf8'),
  readFile(resolve(root, '.cache/freecad/FreeCAD/src/Base/Stream.cpp'), 'utf8'),
  readFile(resolve(root, '.cache/freecad/FreeCAD/src/Base/Color.h'), 'utf8'),
])
if (!propertySource.includes('PropertyColorList::getPyValue') || !propertySource.includes('PropertyColor col;') || !propertySource.includes('<ColorList file=') || !propertySource.includes('uint32_t uCt = (uint32_t)getSize()') || !propertySource.includes('str << it.getPackedValue()') || !propertySource.includes('it.a = 1.0F - it.a')) fail('locked PropertyColorList source no longer exposes the recorded setter/storage contract')
if (!propertyBase.includes('if (PyDict_Check(value))') || !propertyBase.includes('if (PySequence_Check(value))') || !propertyBase.includes('PyObject_GetIter(value)') || !propertyBase.includes('vals.push_back(value)')) fail('locked PropertyLists source no longer exposes the recorded collection inputs')
if (!streamSource.includes('Stream::Stream() = default') || !streamSource.includes('return _swap ? BigEndian : LittleEndian') || !streamSource.includes('OutputStream::operator<<(uint32_t ui)')) fail('locked Base::Stream source no longer exposes the recorded binary encoding')
if (!colorHeader.includes('0xRRGGBBAA')) fail('locked Base::Color source no longer exposes the packed element contract')
console.log(JSON.stringify({ status: 'freecad-property-colorlist-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecords: report.records.filter(({ valueModel }) => valueModel.writable).length, defaultValue: report.records[0].defaultValue, storage: report.storage, classification: report.classification }, null, 2))
