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

const hosts: Array<[string, string, string, ColorValue]> = [
  ['FeatureTestColor', 'App::FeatureTest', 'Colour', [0, 0, 0, 1]],
  ['FeatureTestExceptionColor', 'App::FeatureTestException', 'Colour', [0, 0, 0, 1]],
  ['PartColor', 'App::Part', 'Color', [1, 1, 1, 0]],
  ['AssemblyLinkColor', 'Assembly::AssemblyLink', 'Color', [1, 1, 1, 0]],
  ['AssemblyObjectColor', 'Assembly::AssemblyObject', 'Color', [1, 1, 1, 0]],
  ['AnnotationColor', 'TechDraw::DrawViewAnnotation', 'TextColor', [0, 0, 0, 1]],
  ['DraftColor', 'TechDraw::DrawViewDraft', 'Color', [0, 0, 0, 1]],
  ['SpreadsheetColor', 'TechDraw::DrawViewSpreadsheet', 'TextColor', [0, 0, 0, 1]],
]

const colorProperty = (name: string, value: ColorValue): ObjectPropertySnapshot => ({ name, label: name, group: 'Color', scope: 'data', type: 'App::PropertyColor', value: [...value] as ColorValue, recompute: true })

const fixture = (): DocumentSnapshot => ({
  id: 'property-color-facade',
  label: 'PropertyColor 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, propertyName, value], index) => ({
    id,
    typeId,
    properties: [
      colorProperty(propertyName, value),
      ...(index === 0 ? [{ name: 'LineColor', label: 'Line color', group: 'Appearance', scope: 'view' as const, type: 'App::PropertyColor' as const, value: '#123456' }] : []),
    ],
  })),
  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, propertyName: string) => facade.app.document.getObject(objectId)?.properties.find((property) => property.name === propertyName)?.value

test('App::PropertyColor data values use four RGBA channels while view values retain #RRGGBB', () => {
  const facade = createWebCadFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['FeatureTestColor'], 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: ColorValue = [17 / 255, 34 / 255, 51 / 255, 68 / 255]
  facade.app.document.setProperty({ objectId: 'FeatureTestColor', propertyName: 'Colour', value: target })
  target[0] = 1
  assert.deepEqual(valueOf(facade, 'FeatureTestColor', 'Colour'), [17 / 255, 34 / 255, 51 / 255, 68 / 255])
  const exposed = valueOf(facade, 'FeatureTestColor', 'Colour') as number[]
  exposed[1] = 1
  assert.deepEqual(valueOf(facade, 'FeatureTestColor', 'Colour'), [17 / 255, 34 / 255, 51 / 255, 68 / 255])
  assert.equal(facade.app.document.getActive().version, 2)
  assert.equal(facade.app.document.getActive().recompute?.objectStates.FeatureTestColor, 'touched')
  assert.deepEqual(events.map((event) => event.type), ['property.before-change', 'property.changed', 'transaction.committed'])

  facade.app.document.setProperty({ objectId: 'FeatureTestColor', propertyName: 'Colour', value: [2, -1, 0.5, 1.5] })
  assert.deepEqual(valueOf(facade, 'FeatureTestColor', 'Colour'), [2, -1, 0.5, 1.5])
  facade.app.document.setProperty({ objectId: 'FeatureTestColor', propertyName: 'LineColor', value: '#abcdef' })
  assert.equal(valueOf(facade, 'FeatureTestColor', 'LineColor'), '#abcdef')

  const stable = JSON.stringify(facade.app.document.getActive())
  assert.throws(() => facade.app.document.setProperty({ objectId: 'FeatureTestColor', propertyName: 'Colour', value: '#112233' }), /four finite RGBA channels/)
  assert.throws(() => facade.app.document.setProperty({ objectId: 'FeatureTestColor', propertyName: 'Colour', value: [0, 0, 0] }), /four finite RGBA channels/)
  assert.throws(() => facade.app.document.setProperty({ objectId: 'FeatureTestColor', propertyName: 'Colour', value: [0, 0, 0, Number.NaN] }), /four finite RGBA channels/)
  assert.throws(() => facade.app.document.setProperty({ objectId: 'FeatureTestColor', propertyName: 'LineColor', value: [0, 0, 0, 1] }), /#RRGGBB color/)
  assert.equal(JSON.stringify(facade.app.document.getActive()), stable)
})

test('App::PropertyColor serializes all eight native hosts with packed RRGGBBAA values', () => {
  const document = fixture()
  ;(document.objects[0].properties[0].value as ColorValue) = [17 / 255, 34 / 255, 51 / 255, 68 / 255]
  const archive = serializeFcstdMetadataArchive(document)
  const documentXml = new TextDecoder().decode(unzipSync(archive)['Document.xml'])
  const guiXml = new TextDecoder().decode(unzipSync(archive)['GuiDocument.xml'])
  assert.equal((documentXml.match(/type="App::PropertyColor"/g) ?? []).length, 8)
  assert.match(documentXml, /<Property name="Colour" type="App::PropertyColor"><PropertyColor value="287454020"\/><\/Property>/)
  assert.match(documentXml, /<Property name="Color" type="App::PropertyColor"><PropertyColor value="4294967040"\/><\/Property>/)
  assert.match(documentXml, /<Property name="TextColor" type="App::PropertyColor"><PropertyColor value="255"\/><\/Property>/)
  assert.doesNotMatch(documentXml, /<Property name="(?:Colour|Color|TextColor)"[^>]+(?:group|doc|attr|ro|hide)=/)
  assert.match(guiXml, /<Property name="LineColor" type="App::PropertyColor" status="1"><PropertyColor value="305420031"\/><\/Property>/)

  const inspection = createWebCadFacade({ initialDocument: document, runtimeMode: 'mock' }).project.fcstd.inspect(archive)
  assert.equal(inspection.objects.length, 8)
  assert.ok(inspection.objects.every((object) => object.support === 'recognized'))
  const summary = inspection.objects[0].properties.find((property) => property.name === 'Colour')
  assert.deepEqual({ typeId: summary?.typeId, element: summary?.element, value: summary?.value }, { typeId: 'App::PropertyColor', element: 'PropertyColor', value: '287454020' })
  assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: [17 / 255, 34 / 255, 51 / 255, 68 / 255], decoded: true })
  assert.equal(decodeFcstdPropertyValue({ name: 'Colour', typeId: 'App::PropertyColor', element: 'PropertyColor', value: '4294967296' }).decoded, false)
  assert.equal(decodeFcstdPropertyValue({ name: 'Colour', typeId: 'App::PropertyColor', element: 'String', value: '287454020' }).decoded, false)

  const target: ColorValue = [51 / 255, 102 / 255, 153 / 255, 204 / 255]
  const rewritten = createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.rewriteColor(archive, {
    objectName: 'FeatureTestColor',
    propertyName: 'Colour',
    value: target,
    expectedValue: [17 / 255, 34 / 255, 51 / 255, 68 / 255],
  })
  const rewrittenProperty = createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.inspect(rewritten).objects[0].properties.find((property) => property.name === 'Colour')
  assert.equal(rewrittenProperty?.value, '862362060')
  assert.deepEqual(decodeFcstdPropertyValue(rewrittenProperty!), { value: target, decoded: true })
  assert.throws(() => createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.rewriteColor(archive, { objectName: 'FeatureTestColor', propertyName: 'Colour', value: target, expectedValue: [0, 0, 0, 1] }), /does not match expectedValue/)
  assert.throws(() => createWebCadFacade({ runtimeMode: 'mock' }).project.fcstd.rewriteColor(archive, { objectName: 'FeatureTestColor', propertyName: 'Colour', value: [0, 0, 0, Number.NaN] }), /four finite RGBA channels/)
})
