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

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

type PropertyColorReport = {
  schemaVersion: 1
  status: 'running' | 'pass' | 'failed'
  browserId: 'chrome'
  userAgent: string
  crossOriginIsolated: boolean
  worker?: WorkerTrace
  ui?: { rgbInputType: string; alphaInputType: string; rgbLabel: string; alphaLabel: string; beforeHex: string; afterHex: string; beforeAlpha: string; afterAlpha: number; beforeValue: ColorValue; afterValue: ColorValue; documentVersion: number; dirty: boolean; objectState: string | null; errors: string[] }
  persistence?: { mode: string; sqliteWasm: boolean; opfs: boolean; savedMode: string; loadedValue: ColorValue | null; loadedType: string | null; checkpointVersion: number | null; recoveryIntegrity: string; fcstdElement: string | null; fcstdValue: string | null }
  resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean }
  release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean }
  error?: string
}

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

const colorProperty = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('ColorProbe')?.properties.find((property) => property.name === 'Colour')

function HarnessEditor({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) {
  const [property, setProperty] = useState<ObjectPropertySnapshot | null>(() => colorProperty(facade) ?? null)
  useEffect(() => facade.subscribe((event) => {
    if (event.type === 'state.changed') setProperty(colorProperty(facade) ?? null)
  }), [facade])
  if (!property) return <div data-harness-status="missing-property">Color 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 probe</h2></div></div><div className="properties-scroll"><div className="property-group"><div className="property-group-title">Test</div><PropertyEditor facade={facade} objectId="ColorProbe" 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: ColorValue = [0.2, 0.4, 0.6, 0.8]
const report: PropertyColorReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true }
const errors: string[] = []
const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['ColorProbe'] })
const root = createRoot(document.getElementById('root')!)
root.render(<HarnessEditor facade={facade} errors={errors} />)

try {
  await waitFor(() => document.querySelector<HTMLInputElement>('input[aria-label="Colour RGB"]') !== null && document.querySelector<HTMLInputElement>('input[aria-label="Colour alpha"]') !== null, 'the production PropertyColor editor')
  await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker')
  const rgb = document.querySelector<HTMLInputElement>('input[aria-label="Colour RGB"]')!
  const initialAlpha = document.querySelector<HTMLInputElement>('input[aria-label="Colour alpha"]')!
  const beforeHex = rgb.value
  const beforeAlpha = initialAlpha.value
  const beforeValue = [...(colorProperty(facade)?.value as ColorValue)] as ColorValue
  rgb.focus()
  setNativeInputValue(rgb, '#336699')
  rgb.dispatchEvent(new Event('input', { bubbles: true }))
  rgb.dispatchEvent(new Event('change', { bubbles: true }))
  await waitFor(() => facade.app.document.getActive().version === 2, 'the RGB UI property commit')
  const alpha = document.querySelector<HTMLInputElement>('input[aria-label="Colour alpha"]')!
  alpha.focus()
  alpha.value = '0.8'
  alpha.dispatchEvent(new Event('input', { bubbles: true }))
  alpha.blur()
  await waitFor(() => facade.app.document.getActive().version === 3, 'the alpha UI property commit')
  const edited = facade.app.document.getActive()
  const editedProperty = colorProperty(facade)
  report.ui = {
    rgbInputType: rgb.type,
    alphaInputType: alpha.type,
    rgbLabel: rgb.getAttribute('aria-label') ?? '',
    alphaLabel: alpha.getAttribute('aria-label') ?? '',
    beforeHex,
    afterHex: document.querySelector<HTMLInputElement>('input[aria-label="Colour RGB"]')?.value ?? '',
    beforeAlpha,
    afterAlpha: (editedProperty?.value as ColorValue)[3],
    beforeValue,
    afterValue: [...(editedProperty?.value as ColorValue)] as ColorValue,
    documentVersion: edited.version,
    dirty: edited.dirty,
    objectState: edited.recompute?.objectStates.ColorProbe ?? 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 === 'ColorProbe')?.properties.find((property) => property.name === 'Colour')
  const archive = facade.project.fcstd.serializeMetadata(edited)
  const archiveProperty = facade.project.fcstd.inspect(archive).objects.find((object) => object.name === 'ColorProbe')?.properties.find((property) => property.name === 'Colour')
  const capabilities = facade.project.capabilities()
  const loadedValue = loadedProperty?.value
  report.persistence = {
    mode: capabilities.mode,
    sqliteWasm: capabilities.sqliteWasm,
    opfs: capabilities.opfs,
    savedMode: saved.mode,
    loadedValue: Array.isArray(loadedValue) && loadedValue.length === 4 && loadedValue.every((entry) => typeof entry === 'number' && Number.isFinite(entry)) ? [...loadedValue] as ColorValue : null,
    loadedType: loadedProperty?.type ?? null,
    checkpointVersion: checkpoint?.version ?? null,
    recoveryIntegrity: recovery.integrity,
    fcstdElement: archiveProperty?.element ?? null,
    fcstdValue: archiveProperty?.value ?? null,
  }

  const payload = new TextEncoder().encode('PropertyColor Chrome resource')
  const stored = await facade.project.resource.put(payload, 'application/vnd.freecad.property-color+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-color-chrome-marker.json'
  const marker = await directory.getFileHandle(markerName, { create: true })
  const writable = await marker.createWritable()
  await writable.write(JSON.stringify({ suite: 'PROP-COLOR-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: ColorValue | null | undefined, right: ColorValue) => Array.isArray(left) && left.length === right.length && left.every((entry, index) => entry === right[index])
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?.rgbInputType === 'color' && report.ui.alphaInputType === 'number' && report.ui.rgbLabel === 'Colour RGB' && report.ui.alphaLabel === 'Colour alpha' && report.ui.beforeHex === '#000000' && report.ui.afterHex === '#336699' && report.ui.beforeAlpha === '1' && report.ui.afterAlpha === 0.8 && same(report.ui.beforeValue, [0, 0, 0, 1]) && same(report.ui.afterValue, target) && report.ui.documentVersion === 3 && 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::PropertyColor' && report.persistence.checkpointVersion === 3 && report.persistence.recoveryIntegrity === 'ok' && report.persistence.fcstdElement === 'PropertyColor' && report.persistence.fcstdValue === '862362060'
  && 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-color-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyColor harness failed.')

export {}
