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, DocumentSnapshot, IncludedFileValue, ObjectPropertySnapshot } from './facade/types'
import './styles.css'

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

type PropertyFileIncludedReport = {
  schemaVersion: 1
  status: 'running' | 'pass' | 'failed'
  browserId: 'chrome'
  userAgent: string
  crossOriginIsolated: boolean
  worker?: WorkerTrace
  ui?: { inputType: string; label: string; beforeName: string; afterName: string; afterValue: IncludedFileValue | null; documentVersion: number; dirty: boolean; objectState: string | null; notices: string[] }
  persistence?: { mode: string; sqliteWasm: boolean; opfs: boolean; savedMode: string; loadedValue: IncludedFileValue | null; loadedType: string | null; checkpointVersion: number | null; recoveryIntegrity: string; fcstdElement: string | null; fcstdResourcePath: string | null; fcstdResourceBytes: number; fcstdResourceMatches: boolean }
  resource?: { documentByteLength: number; documentRoundTrip: boolean; independentByteLength: number; independentRoundTrip: boolean; independentReleased: boolean; markerRemoved: boolean }
  release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean }
  error?: string
}

const fixture = (): DocumentSnapshot => ({
  id: 'chrome-property-fileincluded',
  label: 'Chrome PropertyFileIncluded',
  version: 1,
  dirty: false,
  readOnly: false,
  units: 'mm',
  tree: [{ id: 'IncludedProbe', label: 'Included file probe', type: 'feature', state: 'valid' }],
  objects: [{
    id: 'IncludedProbe',
    typeId: 'App::DocumentObjectFileIncluded',
    properties: [{ name: 'File', label: 'Embedded file', group: 'Resource', scope: 'data', type: 'App::PropertyFileIncluded', value: null, recompute: true }],
  }],
  dependencies: [],
  recompute: { generation: 0, status: 'idle', objectStates: { IncludedProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] },
})

const includedProperty = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('IncludedProbe')?.properties.find((property) => property.name === 'File')
const includedValue = (facade: BitBybitWebCadFacade) => includedProperty(facade)?.value as IncludedFileValue | null

function HarnessEditor({ facade, notices }: { facade: BitBybitWebCadFacade; notices: string[] }) {
  const [property, setProperty] = useState<ObjectPropertySnapshot | null>(() => includedProperty(facade) ?? null)
  useEffect(() => facade.subscribe((event) => {
    if (event.type === 'state.changed') setProperty(includedProperty(facade) ?? null)
  }), [facade])
  if (!property) return <div data-harness-status="missing-property">Included file 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>Included file probe</h2></div></div><div className="properties-scroll"><div className="property-group"><div className="property-group-title">Resource</div><PropertyEditor facade={facade} objectId="IncludedProbe" property={property} showNotice={(message) => notices.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 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 targetName = 'chrome-resource.bin'
const targetBytes = new Uint8Array([67, 104, 114, 111, 109, 101, 0, 255, 128, 64, 32, 16, 8, 4, 2, 1])
const report: PropertyFileIncludedReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true }
const notices: string[] = []
const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['IncludedProbe'] })
const root = createRoot(document.getElementById('root')!)
root.render(<HarnessEditor facade={facade} notices={notices} />)

try {
  await waitFor(() => document.querySelector<HTMLInputElement>('input[aria-label="Embedded file"]') !== null, 'the production PropertyFileIncluded editor')
  await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker')
  const input = document.querySelector<HTMLInputElement>('input[aria-label="Embedded file"]')!
  const beforeName = document.querySelector('.property-included-file > span')?.textContent ?? ''
  const transfer = new DataTransfer()
  transfer.items.add(new File([targetBytes], targetName, { type: 'application/octet-stream' }))
  input.files = transfer.files
  input.dispatchEvent(new Event('change', { bubbles: true }))
  await waitFor(() => facade.app.document.getActive().version === 2 && notices.length > 0, 'the included-file property commit')
  const edited = facade.app.document.getActive()
  const value = includedValue(facade)
  report.ui = {
    inputType: input.type,
    label: input.getAttribute('aria-label') ?? '',
    beforeName,
    afterName: document.querySelector('.property-included-file > span')?.textContent ?? '',
    afterValue: value,
    documentVersion: edited.version,
    dirty: edited.dirty,
    objectState: edited.recompute?.objectStates.IncludedProbe ?? null,
    notices,
  }
  if (!value) throw new Error('PropertyFileIncluded UI did not commit a structured value.')

  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 === 'IncludedProbe')?.properties.find((property) => property.name === 'File')
  const archive = await facade.project.fcstd.serializeMetadataWithResources(edited)
  const archiveProperty = facade.project.fcstd.inspect(archive).objects.find((object) => object.name === 'IncludedProbe')?.properties.find((property) => property.name === 'File')
  const archiveFiles = unzipSync(archive)
  const archiveResource = archiveFiles[targetName]
  const capabilities = facade.project.capabilities()
  report.persistence = {
    mode: capabilities.mode,
    sqliteWasm: capabilities.sqliteWasm,
    opfs: capabilities.opfs,
    savedMode: saved.mode,
    loadedValue: loadedProperty?.value as IncludedFileValue | null,
    loadedType: loadedProperty?.type ?? null,
    checkpointVersion: checkpoint?.version ?? null,
    recoveryIntegrity: recovery.integrity,
    fcstdElement: archiveProperty?.element ?? null,
    fcstdResourcePath: archiveProperty?.resourcePath ?? null,
    fcstdResourceBytes: archiveResource?.byteLength ?? 0,
    fcstdResourceMatches: archiveResource?.byteLength === targetBytes.byteLength && archiveResource.every((byte, index) => byte === targetBytes[index]),
  }

  const documentResource = await facade.project.resource.get(value.resourceHash)
  const independentPayload = new TextEncoder().encode('PropertyFileIncluded Chrome independent resource')
  const independent = await facade.project.resource.put(independentPayload, 'text/plain')
  const independentRestored = await facade.project.resource.get(independent.hash)
  await facade.project.resource.release(independent.hash)
  const independentReleased = await facade.project.resource.get(independent.hash) === null
  const directory = await navigator.storage.getDirectory()
  const markerName = 'property-fileincluded-chrome-marker.json'
  const marker = await directory.getFileHandle(markerName, { create: true })
  const writable = await marker.createWritable()
  await writable.write(JSON.stringify({ suite: 'PROP-FILEINCLUDED-H', archiveName: targetName }))
  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 = {
    documentByteLength: documentResource?.byteLength ?? 0,
    documentRoundTrip: documentResource?.byteLength === targetBytes.byteLength && documentResource.every((byte, index) => byte === targetBytes[index]),
    independentByteLength: independent.byteLength,
    independentRoundTrip: independentRestored !== null && new TextDecoder().decode(independentRestored) === new TextDecoder().decode(independentPayload),
    independentReleased,
    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 requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
const value = report.ui?.afterValue
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?.inputType === 'file' && report.ui.label === 'Embedded file' && report.ui.beforeName === 'Empty' && report.ui.afterName === targetName && value?.schemaVersion === 1 && value.archiveName === targetName && /^[a-f0-9]{64}$/.test(value.resourceHash) && value.byteLength === targetBytes.byteLength && value.mediaType === 'application/octet-stream' && report.ui.documentVersion === 2 && report.ui.dirty && report.ui.objectState === 'touched' && report.ui.notices.length === 1 && report.ui.notices[0] === `${targetName} embedded`
  && report.persistence?.mode === 'sqlite-opfs' && report.persistence.sqliteWasm && report.persistence.opfs && report.persistence.savedMode === 'sqlite-opfs' && JSON.stringify(report.persistence.loadedValue) === JSON.stringify(value) && report.persistence.loadedType === 'App::PropertyFileIncluded' && report.persistence.checkpointVersion === 2 && report.persistence.recoveryIntegrity === 'ok' && report.persistence.fcstdElement === 'FileIncluded' && report.persistence.fcstdResourcePath === targetName && report.persistence.fcstdResourceBytes === targetBytes.byteLength && report.persistence.fcstdResourceMatches
  && report.resource?.documentByteLength === targetBytes.byteLength && report.resource.documentRoundTrip && report.resource.independentByteLength > 0 && report.resource.independentRoundTrip && report.resource.independentReleased && 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-fileincluded-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyFileIncluded harness failed.')

export {}
