import { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { unzipSync } from 'fflate'
import { PropertyEditor } from './App'
import { createProductionFacade } from './facade/productionFacade'
import type { BitBybitWebCadFacade, ColorListValue, DocumentSnapshot, ObjectPropertySnapshot } from './facade/types'
import './styles.css'

type WorkerTrace = {
  constructed: Array<{ name: string; url: string }>
  requestTypes: string[]
  terminated: string[]
}

type PropertyColorListReport = {
  schemaVersion: 1
  status: 'running' | 'pass' | 'failed'
  browserId: 'chrome'
  userAgent: string
  crossOriginIsolated: boolean
  worker?: WorkerTrace
  ui?: {
    rgbInputTypes: string[]
    alphaInputTypes: string[]
    rgbLabels: string[]
    alphaLabels: string[]
    addLabel: string
    removeLabel: string
    beforeHex: string[]
    afterHex: string[]
    beforeAlpha: string[]
    afterAlpha: number[]
    beforeValue: ColorListValue
    afterValue: ColorListValue
    beforeItemCount: number
    peakItemCount: number
    afterItemCount: number
    documentVersion: number
    dirty: boolean
    objectState: string | null
    errors: string[]
  }
  persistence?: {
    mode: string
    sqliteWasm: boolean
    opfs: boolean
    savedMode: string
    loadedValue: ColorListValue | null
    loadedType: string | null
    checkpointVersion: number | null
    recoveryIntegrity: string
    fcstdElement: string | null
    fcstdValue: string | null
    fcstdResourcePath: string | null
    fcstdResourceBytes: number
  }
  resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean }
  release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean }
  error?: string
}

const fixture = (): DocumentSnapshot => ({
  id: 'chrome-property-colorlist',
  label: 'Chrome PropertyColorList',
  version: 1,
  dirty: false,
  readOnly: false,
  units: 'mm',
  tree: [{ id: 'ColorListProbe', label: 'Color list probe', type: 'feature', state: 'valid' }],
  objects: [{
    id: 'ColorListProbe',
    typeId: 'App::FeatureTest',
    properties: [{ name: 'ColourList', label: 'Colour list', group: 'Test', scope: 'data', type: 'App::PropertyColorList', value: [[0, 0, 0, 1]], recompute: true }],
  }],
  dependencies: [],
  recompute: { generation: 0, status: 'idle', objectStates: { ColorListProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] },
})

const colorListProperty = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('ColorListProbe')?.properties.find((property) => property.name === 'ColourList')
const clone = (value: ColorListValue): ColorListValue => value.map((color) => [...color])

function HarnessEditor({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) {
  const [property, setProperty] = useState<ObjectPropertySnapshot | null>(() => colorListProperty(facade) ?? null)
  useEffect(() => facade.subscribe((event) => {
    if (event.type === 'state.changed') setProperty(colorListProperty(facade) ?? null)
  }), [facade])
  if (!property) return <div data-harness-status="missing-property">Color list property is missing.</div>
  return <section className="combo-property" data-harness-status="ready"><div className="property-heading"><div><span className="eyebrow">Property view</span><h2>Color list probe</h2></div></div><div className="properties-scroll"><div className="property-group"><div className="property-group-title">Test</div><PropertyEditor facade={facade} objectId="ColorListProbe" property={property} showNotice={(message) => errors.push(message)} /></div></div></section>
}

const waitFor = async (predicate: () => boolean, message: string, timeoutMs = 30_000) => {
  const deadline = performance.now() + timeoutMs
  while (performance.now() < deadline) {
    if (predicate()) return
    await new Promise((resolve) => setTimeout(resolve, 25))
  }
  throw new Error(`Timed out waiting for ${message}.`)
}

const setNativeInputValue = (input: HTMLInputElement, value: string) => {
  const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
  if (!setter) throw new Error('The native HTMLInputElement value setter is unavailable.')
  setter.call(input, value)
}

const trace: WorkerTrace = { constructed: [], requestTypes: [], terminated: [] }
const BrowserWorker = globalThis.Worker
class TrackedWorker extends BrowserWorker {
  private readonly traceName: string

  constructor(scriptURL: string | URL, options?: WorkerOptions) {
    super(scriptURL, options)
    this.traceName = options?.name ?? 'unnamed'
    trace.constructed.push({ name: this.traceName, url: String(scriptURL) })
  }

  override postMessage(message: unknown, transferOrOptions?: StructuredSerializeOptions | Transferable[]) {
    if (message && typeof message === 'object' && 'type' in message && typeof message.type === 'string') trace.requestTypes.push(message.type)
    if (Array.isArray(transferOrOptions)) super.postMessage(message, transferOrOptions)
    else if (transferOrOptions) super.postMessage(message, transferOrOptions)
    else super.postMessage(message)
  }

  override terminate() {
    trace.terminated.push(this.traceName)
    super.terminate()
  }
}
Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: TrackedWorker })

const target: ColorListValue = [
  [0.2, 0.4, 0.6, 0.8],
  [17 / 255, 34 / 255, 51 / 255, 0.4],
]
const report: PropertyColorListReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true }
const errors: string[] = []
const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['ColorListProbe'] })
const root = createRoot(document.getElementById('root')!)
root.render(<HarnessEditor facade={facade} errors={errors} />)

try {
  await waitFor(() => document.querySelector<HTMLInputElement>('input[aria-label="Colour list 1 RGB"]') !== null && document.querySelector<HTMLButtonElement>('button[aria-label="Add Colour list color"]') !== null, 'the production PropertyColorList editor')
  await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker')
  const beforeRgb = [...document.querySelectorAll<HTMLInputElement>('.property-color-list-item input[type="color"]')]
  const beforeAlphaInputs = [...document.querySelectorAll<HTMLInputElement>('.property-color-list-item input[type="number"]')]
  const beforeHex = beforeRgb.map((input) => input.value)
  const beforeAlpha = beforeAlphaInputs.map((input) => input.value)
  const beforeValue = clone(colorListProperty(facade)?.value as ColorListValue)
  const addButton = document.querySelector<HTMLButtonElement>('button[aria-label="Add Colour list color"]')!
  const addLabel = addButton.getAttribute('aria-label') ?? ''

  addButton.click()
  await waitFor(() => facade.app.document.getActive().version === 2 && document.querySelectorAll('.property-color-list-item').length === 2, 'the first ColorList item addition')
  document.querySelector<HTMLButtonElement>('button[aria-label="Add Colour list color"]')!.click()
  await waitFor(() => facade.app.document.getActive().version === 3 && document.querySelectorAll('.property-color-list-item').length === 3, 'the second ColorList item addition')
  const peakItemCount = document.querySelectorAll('.property-color-list-item').length
  const removeButton = document.querySelector<HTMLButtonElement>('button[aria-label="Remove Colour list 3"]')!
  const removeLabel = removeButton.getAttribute('aria-label') ?? ''
  removeButton.click()
  await waitFor(() => facade.app.document.getActive().version === 4 && document.querySelectorAll('.property-color-list-item').length === 2, 'the ColorList item removal')

  const editRgb = async (index: number, value: string, version: number) => {
    const input = document.querySelector<HTMLInputElement>(`input[aria-label="Colour list ${index + 1} RGB"]`)
    if (!input) throw new Error(`ColorList RGB input ${index + 1} is missing.`)
    input.focus()
    setNativeInputValue(input, value)
    input.dispatchEvent(new Event('input', { bubbles: true }))
    input.dispatchEvent(new Event('change', { bubbles: true }))
    await waitFor(() => facade.app.document.getActive().version === version, `ColorList RGB commit ${index + 1}`)
  }
  const editAlpha = async (index: number, value: string, version: number) => {
    const input = document.querySelector<HTMLInputElement>(`input[aria-label="Colour list ${index + 1} alpha"]`)
    if (!input) throw new Error(`ColorList alpha input ${index + 1} is missing.`)
    input.focus()
    input.value = value
    input.dispatchEvent(new Event('input', { bubbles: true }))
    input.blur()
    await waitFor(() => facade.app.document.getActive().version === version, `ColorList alpha commit ${index + 1}`)
  }
  await editRgb(0, '#336699', 5)
  await editAlpha(0, '0.8', 6)
  await editRgb(1, '#112233', 7)
  await editAlpha(1, '0.4', 8)

  const edited = facade.app.document.getActive()
  const editedProperty = colorListProperty(facade)
  const rgbInputs = [...document.querySelectorAll<HTMLInputElement>('.property-color-list-item input[type="color"]')]
  const alphaInputs = [...document.querySelectorAll<HTMLInputElement>('.property-color-list-item input[type="number"]')]
  report.ui = {
    rgbInputTypes: rgbInputs.map((input) => input.type),
    alphaInputTypes: alphaInputs.map((input) => input.type),
    rgbLabels: rgbInputs.map((input) => input.getAttribute('aria-label') ?? ''),
    alphaLabels: alphaInputs.map((input) => input.getAttribute('aria-label') ?? ''),
    addLabel,
    removeLabel,
    beforeHex,
    afterHex: rgbInputs.map((input) => input.value),
    beforeAlpha,
    afterAlpha: (editedProperty?.value as ColorListValue).map((color) => color[3]),
    beforeValue,
    afterValue: clone(editedProperty?.value as ColorListValue),
    beforeItemCount: beforeRgb.length,
    peakItemCount,
    afterItemCount: rgbInputs.length,
    documentVersion: edited.version,
    dirty: edited.dirty,
    objectState: edited.recompute?.objectStates.ColorListProbe ?? null,
    errors,
  }

  const saved = await facade.project.save()
  const loaded = await facade.project.load(edited.id)
  const checkpoint = await facade.project.loadCheckpoint(edited.id, edited.version)
  const recovery = await facade.project.recovery(edited.id)
  const loadedProperty = loaded?.objects.find((object) => object.id === 'ColorListProbe')?.properties.find((property) => property.name === 'ColourList')
  const archive = facade.project.fcstd.serializeMetadata(edited)
  const archiveProperty = facade.project.fcstd.inspect(archive).objects.find((object) => object.name === 'ColorListProbe')?.properties.find((property) => property.name === 'ColourList')
  const archiveFiles = unzipSync(archive)
  const resourcePath = archiveProperty?.resourcePath ?? ''
  const capabilities = facade.project.capabilities()
  const loadedValue = loadedProperty?.value
  const validLoadedValue = Array.isArray(loadedValue) && loadedValue.every((color) => Array.isArray(color) && color.length === 4 && color.every((channel) => typeof channel === 'number' && Number.isFinite(channel)))
  report.persistence = {
    mode: capabilities.mode,
    sqliteWasm: capabilities.sqliteWasm,
    opfs: capabilities.opfs,
    savedMode: saved.mode,
    loadedValue: validLoadedValue ? clone(loadedValue as ColorListValue) : null,
    loadedType: loadedProperty?.type ?? null,
    checkpointVersion: checkpoint?.version ?? null,
    recoveryIntegrity: recovery.integrity,
    fcstdElement: archiveProperty?.element ?? null,
    fcstdValue: archiveProperty?.value ?? null,
    fcstdResourcePath: resourcePath || null,
    fcstdResourceBytes: resourcePath && archiveFiles[resourcePath] ? archiveFiles[resourcePath].byteLength : 0,
  }

  const payload = new TextEncoder().encode('PropertyColorList Chrome resource')
  const stored = await facade.project.resource.put(payload, 'application/vnd.freecad.property-colorlist+json')
  const restored = await facade.project.resource.get(stored.hash)
  await facade.project.resource.release(stored.hash)
  const released = await facade.project.resource.get(stored.hash) === null
  const directory = await navigator.storage.getDirectory()
  const markerName = 'property-colorlist-chrome-marker.json'
  const marker = await directory.getFileHandle(markerName, { create: true })
  const writable = await marker.createWritable()
  await writable.write(JSON.stringify({ suite: 'PROP-COLORLIST-H', value: target }))
  await writable.close()
  await directory.removeEntry(markerName)
  let markerRemoved = false
  try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' }
  report.resource = { byteLength: stored.byteLength, roundTrip: restored !== null && new TextDecoder().decode(restored) === new TextDecoder().decode(payload), released, markerRemoved }
} catch (error) {
  report.error = error instanceof Error ? error.stack || error.message : String(error)
} finally {
  root.unmount()
  facade.geometry.dispose()
  await facade.project.dispose().catch((error: unknown) => { report.error = `${report.error ? `${report.error}\n` : ''}${error instanceof Error ? error.stack || error.message : String(error)}` })
  Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: BrowserWorker })
  const geometry = facade.geometry.capabilities()
  report.worker = trace
  report.release = { shapeCount: geometry.shapeCount, kernelReferenceCount: geometry.kernelReferenceCount, workerTerminated: trace.terminated.includes('bitbybit-persistence') }
}

const same = (left: ColorListValue | null | undefined, right: ColorListValue) => Array.isArray(left) && left.length === right.length && left.every((color, index) => color.length === right[index]?.length && color.every((channel, channelIndex) => channel === right[index][channelIndex]))
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
report.status = !report.error
  && report.crossOriginIsolated
  && report.worker?.constructed.some(({ name }) => name === 'bitbybit-persistence') === true
  && requiredWorkerRequests.every((type) => report.worker?.requestTypes.includes(type))
  && report.ui?.rgbInputTypes.every((type) => type === 'color') === true && report.ui.alphaInputTypes.every((type) => type === 'number') && report.ui.rgbLabels.join('|') === 'Colour list 1 RGB|Colour list 2 RGB' && report.ui.alphaLabels.join('|') === 'Colour list 1 alpha|Colour list 2 alpha' && report.ui.addLabel === 'Add Colour list color' && report.ui.removeLabel === 'Remove Colour list 3' && report.ui.beforeHex.join('|') === '#000000' && report.ui.afterHex.join('|') === '#336699|#112233' && report.ui.beforeAlpha.join('|') === '1' && report.ui.afterAlpha.join('|') === '0.8|0.4' && same(report.ui.beforeValue, [[0, 0, 0, 1]]) && same(report.ui.afterValue, target) && report.ui.beforeItemCount === 1 && report.ui.peakItemCount === 3 && report.ui.afterItemCount === 2 && report.ui.documentVersion === 8 && report.ui.dirty && report.ui.objectState === 'touched' && report.ui.errors.length === 0
  && report.persistence?.mode === 'sqlite-opfs' && report.persistence.sqliteWasm && report.persistence.opfs && report.persistence.savedMode === 'sqlite-opfs' && same(report.persistence.loadedValue, target) && report.persistence.loadedType === 'App::PropertyColorList' && report.persistence.checkpointVersion === 8 && report.persistence.recoveryIntegrity === 'ok' && report.persistence.fcstdElement === 'ColorList' && report.persistence.fcstdValue === JSON.stringify(target) && report.persistence.fcstdResourcePath === 'ColourList' && report.persistence.fcstdResourceBytes === 12
  && report.resource !== undefined && report.resource.byteLength > 0 && report.resource.roundTrip && report.resource.released && report.resource.markerRemoved
  && report.release?.shapeCount === 0 && report.release.kernelReferenceCount === 0 && report.release.workerTerminated
  ? 'pass' : 'failed'

document.documentElement.dataset.status = report.status
document.body.textContent = JSON.stringify(report, null, 2)
await fetch('/__chrome-property-colorlist-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyColorList harness failed.')

export {}
