import { test } from 'node:test'
import assert from 'node:assert/strict'
import { unzipSync, zipSync } from 'fflate'
import { createWebCadFacade } from '../src/facade/mockFacade'
import { decodeFcstdPropertyValue, serializeFcstdMetadataArchive } from '../src/facade/fcstd'
import type { ColorListValue, DocumentSnapshot, FacadeEvent, ObjectPropertySnapshot } from '../src/facade/types'

const hosts: Array<[string, string]> = [
  ['FeatureTestColorList', 'App::FeatureTest'],
  ['FeatureTestExceptionColorList', 'App::FeatureTestException'],
]

const colorListProperty = (value: ColorListValue): ObjectPropertySnapshot => ({
  name: 'ColourList',
  label: 'Colour list',
  group: 'Color',
  scope: 'data',
  type: 'App::PropertyColorList',
  value: value.map((color) => [...color]),
  recompute: true,
})

const fixture = (): DocumentSnapshot => ({
  id: 'property-colorlist-facade',
  label: 'PropertyColorList Facade',
  version: 1,
  dirty: false,
  readOnly: false,
  units: 'mm',
  tree: hosts.map(([id]) => ({ id, label: id, type: 'feature', state: 'valid' })),
  objects: hosts.map(([id, typeId]) => ({ id, typeId, properties: [colorListProperty([[0, 0, 0, 1]])] })),
  dependencies: [],
  recompute: { generation: 0, status: 'idle', objectStates: Object.fromEntries(hosts.map(([id]) => [id, 'up-to-date'])), dirtyObjects: [], order: [], errors: [] },
})

const valueOf = (facade: ReturnType<typeof createWebCadFacade>, objectId: string) => facade.app.document.getObject(objectId)?.properties.find((property) => property.name === 'ColourList')?.value

test('App::PropertyColorList uses atomic ordered RGBA lists with deep snapshot isolation', () => {
  const facade = createWebCadFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['FeatureTestColorList'], runtimeMode: 'mock' })
  const events: FacadeEvent[] = []
  facade.subscribe((event) => {
    if (event.type === 'property.before-change' || event.type === 'property.changed' || event.type === 'transaction.committed') events.push(event)
  })
  const target: ColorListValue = [
    [0.125, 0.375, 0.625, 0.875],
    [17 / 255, 34 / 255, 51 / 255, 68 / 255],
  ]
  facade.app.document.setProperty({ objectId: 'FeatureTestColorList', propertyName: 'ColourList', value: target })
  target[0][0] = 1
  assert.deepEqual(valueOf(facade, 'FeatureTestColorList'), [
    [0.125, 0.375, 0.625, 0.875],
    [17 / 255, 34 / 255, 51 / 255, 68 / 255],
  ])
  const exposed = valueOf(facade, 'FeatureTestColorList') as ColorListValue
  exposed[1][1] = 1
  assert.deepEqual(valueOf(facade, 'FeatureTestColorList'), [
    [0.125, 0.375, 0.625, 0.875],
    [17 / 255, 34 / 255, 51 / 255, 68 / 255],
  ])
  assert.equal(facade.app.document.getActive().version, 2)
  assert.equal(facade.app.document.getActive().recompute?.objectStates.FeatureTestColorList, 'touched')
  assert.deepEqual(events.map((event) => event.type), ['property.before-change', 'property.changed', 'transaction.committed'])

  const stable = JSON.stringify(facade.app.document.getActive())
  const eventCount = events.length
  assert.throws(() => facade.app.document.setProperty({ objectId: 'FeatureTestColorList', propertyName: 'ColourList', value: [[0, 0, 0, Number.NaN]] }), /list of four finite RGBA channels/)
  assert.throws(() => facade.app.document.setProperty({ objectId: 'FeatureTestColorList', propertyName: 'ColourList', value: [[0, 0, 0], [0, 0, 0, 1]] as unknown as ColorListValue }), /list of four finite RGBA channels/)
  assert.throws(() => facade.app.document.setProperty({ objectId: 'FeatureTestColorList', propertyName: 'ColourList', value: '#112233' }), /list of four finite RGBA channels/)
  assert.equal(JSON.stringify(facade.app.document.getActive()), stable)
  assert.equal(events.length, eventCount)
})

test('App::PropertyColorList uses native external little-endian ColorList resources', () => {
  const document = fixture()
  document.objects[0].properties[0].value = [
    [17 / 255, 34 / 255, 51 / 255, 68 / 255],
    [51 / 255, 102 / 255, 153 / 255, 204 / 255],
  ]
  document.objects[1].properties[0].value = [[0, 0, 0, 1]]
  const archive = serializeFcstdMetadataArchive(document)
  const files = unzipSync(archive)
  const documentXml = new TextDecoder().decode(files['Document.xml'])
  assert.equal((documentXml.match(/type="App::PropertyColorList"/g) ?? []).length, 2)
  assert.match(documentXml, /<Property name="ColourList" type="App::PropertyColorList"><ColorList file="ColourList"\/><\/Property>/)
  assert.match(documentXml, /<Property name="ColourList" type="App::PropertyColorList"><ColorList file="ColourList1"\/><\/Property>/)
  assert.doesNotMatch(documentXml, /<Property name="ColourList"[^>]+(?:group|doc|attr|ro|hide)=/)

  const first = new DataView(files.ColourList.buffer, files.ColourList.byteOffset, files.ColourList.byteLength)
  assert.equal(first.byteLength, 12)
  assert.equal(first.getUint32(0, true), 2)
  assert.equal(first.getUint32(4, true), 0x11223344)
  assert.equal(first.getUint32(8, true), 0x336699cc)
  const second = new DataView(files.ColourList1.buffer, files.ColourList1.byteOffset, files.ColourList1.byteLength)
  assert.equal(second.byteLength, 8)
  assert.equal(second.getUint32(0, true), 1)
  assert.equal(second.getUint32(4, true), 0x000000ff)

  const inspection = createWebCadFacade({ initialDocument: document, runtimeMode: 'mock' }).project.fcstd.inspect(archive)
  assert.ok(inspection.objects.every((object) => object.support === 'recognized'))
  const summaries = inspection.objects.map((object) => object.properties.find((property) => property.name === 'ColourList'))
  assert.ok(summaries.every((summary) => summary?.typeId === 'App::PropertyColorList' && summary.element === 'ColorList'))
  assert.deepEqual(decodeFcstdPropertyValue(summaries[0]!), { value: document.objects[0].properties[0].value, decoded: true })
  assert.deepEqual(decodeFcstdPropertyValue(summaries[1]!), { value: document.objects[1].properties[0].value, decoded: true })
  assert.equal(decodeFcstdPropertyValue({ name: 'ColourList', typeId: 'App::PropertyColorList', element: 'String', value: '[]' }).decoded, false)

  const directlyRewritten = createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.rewriteColorList(archive, {
    objectName: 'FeatureTestColorList',
    propertyName: 'ColourList',
    value: [[1, 0.5, 0.25, 0]],
    expectedValue: document.objects[0].properties[0].value as ColorListValue,
  })
  const directlyRewrittenSummary = createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.inspect(directlyRewritten).objects[0].properties.find((property) => property.name === 'ColourList')
  assert.deepEqual(decodeFcstdPropertyValue(directlyRewrittenSummary!), { value: [[1, 128 / 255, 64 / 255, 0]], decoded: true })
  assert.throws(() => createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.rewriteColorList(archive, { objectName: 'FeatureTestColorList', propertyName: 'ColourList', value: [], expectedValue: [[0, 0, 0, 1]] }), /does not match expectedValue/)
  assert.throws(() => createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.rewriteColorList(archive, { objectName: 'FeatureTestColorList', propertyName: 'ColourList', value: [[0, 0, 0, Number.NaN]] }), /list of four finite RGBA channels/)

  const replacement: ColorListValue = [[1, 0.5, 0.25, 0]]
  document.objects[0].properties[0].value = replacement
  const rewritten = createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.rewriteMetadata(archive, document)
  const rewrittenSummary = createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.inspect(rewritten).objects[0].properties.find((property) => property.name === 'ColourList')
  assert.deepEqual(decodeFcstdPropertyValue(rewrittenSummary!), { value: [
    [1, 128 / 255, 64 / 255, 0],
  ], decoded: true })
  assert.equal(unzipSync(rewritten).ColourList.byteLength, 8)

  const emptyDocument = fixture()
  emptyDocument.objects.forEach((object) => { object.properties[0].value = [] })
  const emptyFiles = unzipSync(serializeFcstdMetadataArchive(emptyDocument))
  assert.equal((new TextDecoder().decode(emptyFiles['Document.xml']).match(/<ColorList file=""\/>/g) ?? []).length, 2)
  assert.equal(emptyFiles.ColourList, undefined)
  const emptyInspection = createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.inspect(zipSync(emptyFiles))
  assert.ok(emptyInspection.objects.every((object) => decodeFcstdPropertyValue(object.properties.find((property) => property.name === 'ColourList')!).decoded))

  const missingResource = { ...files }
  delete missingResource.ColourList
  assert.throws(() => createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.inspect(zipSync(missingResource)), /references missing resource: ColourList/)
})
