import { createHash } from 'node:crypto'
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 runtimePath = '.cache/freecad/reference-desktop.json'
const reportPath = 'config/freecad-property-fileincluded-inventory.json'
const fail = (message) => { throw new Error(`FreeCAD PropertyFileIncluded inventory check failed: ${message}`) }
const [runtimeContent, reportContent] = await Promise.all([
  readFile(resolve(root, runtimePath)),
  readFile(resolve(root, reportPath)),
])
const runtime = JSON.parse(runtimeContent)
const report = JSON.parse(reportContent)

if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyFileIncluded' || report.classification !== 'opaque-fcstd-proxy' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('report boundary is invalid')
if (report.recordCount !== 20 || report.objectTypeCount !== 15 || report.records?.length !== 20) fail('runtime record or host count changed')
if (report.provenance?.runtime?.path !== runtimePath || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale')
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 normalizeStatus = (status) => status.map((entry) => entry === 27 ? 'PropOutput' : String(entry))
const nativeMatches = []
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) {
  if (property.typeId !== 'App::PropertyFileIncluded') continue
  nativeMatches.push({
    objectTypeId: objectType.typeId,
    objectAvailable: objectType.available === true,
    probeStatus: objectType.probeStatus,
    propertyName: property.name,
    group: property.group,
    status: property.status,
    normalizedStatus: normalizeStatus(property.status),
    defaultRaw: property.default,
  })
}
const identity = ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, normalizedStatus, defaultRaw }) => ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, normalizedStatus, defaultRaw })
const sortRecords = (records) => records.map(identity).sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`))
if (nativeMatches.length !== 20 || !isDeepStrictEqual(sortRecords(nativeMatches), sortRecords(report.records))) fail('report diverges from the locked runtime oracle')

const expectedKeys = [
  'App::DocumentObjectFileIncluded.File',
  'App::VRMLObject.VrmlFile',
  'Image::ImagePlane.ImageFile',
  'Robot::RobotObject.RobotKinematicFile',
  'Robot::RobotObject.RobotVrmlFile',
  'Sketcher::SketchObjectSF.SketchFlatFile',
  'TechDraw::DrawComplexSection.PatIncluded',
  'TechDraw::DrawComplexSection.SvgIncluded',
  'TechDraw::DrawComplexSectionPython.PatIncluded',
  'TechDraw::DrawComplexSectionPython.SvgIncluded',
  'TechDraw::DrawGeomHatch.PatIncluded',
  'TechDraw::DrawHatch.SvgIncluded',
  'TechDraw::DrawSVGTemplate.PageResult',
  'TechDraw::DrawTileWeld.SymbolIncluded',
  'TechDraw::DrawTileWeldPython.SymbolIncluded',
  'TechDraw::DrawViewImage.ImageIncluded',
  'TechDraw::DrawViewSection.PatIncluded',
  'TechDraw::DrawViewSection.SvgIncluded',
  'TechDraw::DrawViewSectionPython.PatIncluded',
  'TechDraw::DrawViewSectionPython.SvgIncluded',
]
if (report.records.map(({ objectTypeId, propertyName }) => `${objectTypeId}.${propertyName}`).join('|') !== expectedKeys.join('|')) fail('host/property inventory changed')
if (report.records.some((record) => record.objectAvailable !== true || record.probeStatus !== 'available' || record.valueModel?.kind !== 'included-file-resource' || record.valueModel.representation !== 'read-only document-transient file backed by embedded FCStd bytes' || record.valueModel.pathIsRuntimeOnly !== true || record.valueModel.bytesAreAuthoritative !== true || record.inputs?.sourceFileMustExist !== true || record.inputs.emptyStringBehavior !== 'accepted-no-op; does not clear an existing value' || record.inputs.sameTransientPathBehavior !== 'rejected' || record.editor !== 'Gui::PropertyEditor::PropertyTransientFileItem' || record.applicability?.requiredObjectTypeId !== record.objectTypeId || record.applicability.propertyWriteRequiresShape !== false)) fail('per-record value, input, editor or applicability contract is incomplete')
if (report.records.filter(({ valueModel }) => valueModel.writableByStatus).length !== 11 || report.records.filter(({ status }) => isDeepStrictEqual(status, ['ReadOnly'])).length !== 8 || report.records.filter(({ status }) => isDeepStrictEqual(status, [27])).length !== 1 || report.records.filter(({ defaultKind }) => defaultKind === 'empty').length !== 8 || report.records.filter(({ defaultKind }) => defaultKind === 'document-transient-copy').length !== 12) fail('status/default partition changed')

const dependencyPairs = report.records.filter(({ dependency }) => dependency !== null).map(({ objectTypeId, propertyName, dependency }) => `${objectTypeId}.${propertyName}<-${dependency.sourceProperty}`)
const expectedDependencyPairs = [
  'TechDraw::DrawComplexSection.PatIncluded<-FileGeomPattern',
  'TechDraw::DrawComplexSection.SvgIncluded<-FileHatchPattern',
  'TechDraw::DrawComplexSectionPython.PatIncluded<-FileGeomPattern',
  'TechDraw::DrawComplexSectionPython.SvgIncluded<-FileHatchPattern',
  'TechDraw::DrawGeomHatch.PatIncluded<-FilePattern',
  'TechDraw::DrawHatch.SvgIncluded<-HatchPattern',
  'TechDraw::DrawSVGTemplate.PageResult<-Template',
  'TechDraw::DrawTileWeld.SymbolIncluded<-SymbolFile',
  'TechDraw::DrawTileWeldPython.SymbolIncluded<-SymbolFile',
  'TechDraw::DrawViewImage.ImageIncluded<-ImageFile',
  'TechDraw::DrawViewSection.PatIncluded<-FileGeomPattern',
  'TechDraw::DrawViewSection.SvgIncluded<-FileHatchPattern',
  'TechDraw::DrawViewSectionPython.PatIncluded<-FileGeomPattern',
  'TechDraw::DrawViewSectionPython.SvgIncluded<-FileHatchPattern',
]
if (!isDeepStrictEqual(dependencyPairs, expectedDependencyPairs) || report.records.filter(({ dependency }) => dependency === null).length !== 6 || report.records.some(({ dependency }) => dependency && dependency.relation !== 'host-onChanged-copies-source-bytes-into-included-property')) fail('derived-source dependency inventory changed')

const setter = report.setterSemantics
if (setter?.externalSource !== 'copy bytes into the document transient directory' || setter.writableTransientSource !== 'rename into the selected transient destination' || setter.replacement !== 'delete the previous transient copy unless undo owns it' || setter.destinationPermissions !== 'read-only' || setter.archiveNameCollision !== 'append a positive integer before the extension' || setter.undoRedo !== 'Copy/Paste move or copy transient files while preserving per-property ownership') fail('setter and ownership semantics are incomplete')
const storage = report.storage
if (storage?.xmlElement !== 'FileIncluded' || storage.archiveAttribute !== 'file' || storage.archiveResource !== 'separate FCStd ZIP entry containing byte-identical file data' || storage.forceXmlAttribute !== 'data' || storage.forceXmlResource !== 'binary data embedded in the XML stream' || storage.emptyEncoding !== '<FileIncluded file=""/>' || storage.restoredValue !== 'document transient path; not a portable semantic path' || storage.browserBoundary !== 'project resource identity plus bytes; never expose or persist an ambient host path') fail('FCStd or browser storage boundary is incomplete')

const sources = new Map(await Promise.all((report.provenance?.sources ?? []).map(async ({ path }) => [path, await readFile(resolve(root, path), 'utf8')])))
const propertyHeader = sources.get('.cache/freecad/FreeCAD/src/App/PropertyFile.h') ?? ''
const propertySource = sources.get('.cache/freecad/FreeCAD/src/App/PropertyFile.cpp') ?? ''
const editorHeader = sources.get('.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h') ?? ''
const editorSource = sources.get('.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp') ?? ''
if (!propertyHeader.includes('class AppExport PropertyFileIncluded: public Property') || !propertyHeader.includes('return "Gui::PropertyEditor::PropertyTransientFileItem"') || !propertyHeader.includes("it's not allowed to write the file") || !propertySource.includes('TYPESYSTEM_SOURCE(App::PropertyFileIncluded, App::Property)') || !propertySource.includes('Not possible to set the same file!') || !propertySource.includes('str << "File " << file.filePath() << " does not exist."') || !propertySource.includes('PyTuple_Check(value)') || !propertySource.includes('PyDict_Check(value)') || !propertySource.includes('writer.isForceXML()') || !propertySource.includes('<FileIncluded data=') || !propertySource.includes('<FileIncluded file=') || !propertySource.includes('writer.addFile(file.fileName().c_str(), this)') || !propertySource.includes('reader.addFile(file.c_str(), this)') || !propertySource.includes('reader.readBinFile(_cValue.c_str())') || !propertySource.includes('PropertyFileIncluded::SaveDocFile()') || !propertySource.includes('PropertyFileIncluded::Copy()') || !propertySource.includes('PropertyFileIncluded::Paste(') || !editorHeader.includes('class GuiExport PropertyTransientFileItem: public PropertyItem') || !editorSource.includes('PROPERTYITEM_SOURCE(Gui::PropertyEditor::PropertyTransientFileItem)') || !editorSource.includes('Gui::FileChooser')) fail('locked native setter, FCStd, ownership or editor source changed')
for (const [path, snippets] of [
  ['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSection.cpp', ['if (prop == &FileHatchPattern)', 'replaceSvgIncluded(FileHatchPattern.getValue())', 'if (prop == &FileGeomPattern)', 'replacePatIncluded(FileGeomPattern.getValue())']],
  ['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawGeomHatch.cpp', ['if (prop == &FilePattern)', 'PatIncluded.setValue(newHatchFileName.c_str())']],
  ['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawHatch.cpp', ['if (prop == &HatchPattern)', 'SvgIncluded.setValue(newHatchFileName.c_str())']],
  ['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawSVGTemplate.cpp', ['if (prop == &Template && !isRestoring())', 'PageResult.setValue(newTemplateFileName.c_str())']],
  ['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawTileWeld.cpp', ['if (prop == &SymbolFile)', 'SymbolIncluded.setValue(newSymbolFile.c_str())']],
  ['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewImage.cpp', ['if (prop == &ImageFile)', 'ImageIncluded.setValue(newImageFile.c_str())']],
]) for (const snippet of snippets) if (!(sources.get(path) ?? '').includes(snippet)) fail(`locked derived-source contract changed in ${path}`)

console.log(JSON.stringify({ status: 'freecad-property-fileincluded-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, objectTypeCount: report.objectTypeCount, writableByStatus: report.records.filter(({ valueModel }) => valueModel.writableByStatus).length, readOnlyRecords: 8, outputRecords: 1, directRecords: 6, derivedRecords: 14, storage: report.storage, classification: report.classification }, null, 2))
