// Vue↔React usage-level parity gate (D1). See
// docs/superpowers/specs/2026-07-13-vue-react-usage-parity-tool-design.md.
//
//   C gate      — day-one: named-slot risk set (TopBar/FormItem/PillStatus).
//                 Both sides must render the injected slot tokens, and the slot
//                 tokens must be symmetric (structure parity / slot-drop guard).
//   A-narrow    — day-one blocking: independent Logo host height must match ±1px
//                 across SFC↔CE. This is the §6 self-証 target.
//   A-broad     — baseline survey (NON-gating): full computed-style diff per
//                 risk-set entry, attached for Task 8 triage.
//
// Guard: vitest globs tests/**/*.spec.ts — skip there so `vitest run` doesn't
// execute Playwright test() in jsdom. Runs via playwright.framework-parity.config.ts.
import fs from 'node:fs'
import path from 'node:path'
import type { Page } from '@playwright/test'
import type { ManifestEntry } from '../visual-verify/lib/drift-compare-core'
import { collectActual } from '../visual-verify/lib/drift-compare-core'
import { collectVisibleText, collectElementOutline } from './lib/collect-visible'
import { SLOT_FIXTURES } from './slot-fixtures'
import { compareComputedStyle, assertSlotTokensPresent, compareVisibleText } from './lib/parity-compare'

const VUE = 'http://localhost:5173'
const REACT = 'http://localhost:5174'
// Node context (no vite alias) — read the manifest the same way the existing
// Playwright gates do (react-drift-full.spec.ts / manifest-verifier.spec.ts).
const manifest = JSON.parse(
  fs.readFileSync(path.resolve('figma-data/render-verification-manifest.json'), 'utf8'),
) as ManifestEntry[]
const RISK_SET = Object.keys(SLOT_FIXTURES)

// FormItem has 64 manifest entries; cap per-component so the gate stays fast +
// deterministic (same slot fixture per component → a few variants suffice).
// Task 8 can widen the A-broad survey. TopBar (4) / PillStatus (8) fit under
// the cap, so they run in full.
const SAMPLE_PER_COMPONENT = 8

function sampleRiskEntries(all: ManifestEntry[]): ManifestEntry[] {
  const seen: Record<string, number> = {}
  const out: ManifestEntry[] = []
  for (const e of all) {
    if (!RISK_SET.includes(e.codeComponent)) continue
    seen[e.codeComponent] = seen[e.codeComponent] ?? 0
    if (seen[e.codeComponent] >= SAMPLE_PER_COMPONENT) continue
    seen[e.codeComponent]++
    out.push(e)
  }
  return out
}

const riskEntries = sampleRiskEntries(manifest)

if (process.env.VITEST) {
  const { test } = await import('vitest')
  test.skip('framework-parity runs via playwright.framework-parity.config.ts', () => {})
} else {
  const { test, expect } = await import('@playwright/test')

  // Slot injection is opt-in via `?parity=1` on BOTH harnesses; render-verification
  // never passes it, so those gates stay byte-identical.
  async function loadVue(page: Page, entry: ManifestEntry) {
    const sep = entry.renderRoute.includes('?') ? '&' : '?'
    await page.goto(`${VUE}${entry.renderRoute}${sep}parity=1`, { waitUntil: 'networkidle' })
    await page.locator(`[data-manifest-id="${entry.manifestId}"]`).waitFor({ state: 'attached' })
    await page.waitForTimeout(150)
  }
  async function loadReact(page: Page, entry: ManifestEntry) {
    await page.goto(
      `${REACT}/?manifestId=${encodeURIComponent(entry.manifestId)}&theme=${entry.theme ?? 'dark'}&parity=1`,
      { waitUntil: 'networkidle' },
    )
    await page.locator(`[data-manifest-id="${entry.manifestId}"]`).waitFor({ state: 'attached' })
    await page.waitForTimeout(150)
  }

  // ---- C gate: named-slot risk set — both sides render slot tokens + symmetry ----
  for (const entry of riskEntries) {
    test(`C parity [${entry.codeComponent}] ${entry.manifestId} @${entry.theme ?? 'dark'}`, async ({ page }, testInfo) => {
      const expectTokens = SLOT_FIXTURES[entry.codeComponent].expectTokens

      await loadVue(page, entry)
      const vueText = await page.evaluate(collectVisibleText)
      const vueOutline = await page.evaluate(collectElementOutline)

      await loadReact(page, entry)
      const reactText = await page.evaluate(collectVisibleText)
      const reactOutline = await page.evaluate(collectElementOutline)

      // Element outlines are representation-asymmetric across SFC light-DOM vs
      // CE shadow-DOM (customElement count differs) → attached as diagnostic
      // only, NOT gated (would false-positive).
      await testInfo.attach('element-outline', {
        body: JSON.stringify({ vue: vueOutline, react: reactOutline }, null, 2),
        contentType: 'application/json',
      })

      // Both sides must render every expected slot token (slot-drop guard).
      expect(assertSlotTokensPresent(vueText, expectTokens).missing, `Vue missing slot tokens — ${vueText}`).toEqual([])
      expect(assertSlotTokensPresent(reactText, expectTokens).missing, `React missing slot tokens — ${reactText}`).toEqual([])

      // Slot tokens must be symmetric across frameworks (structure parity).
      const diff = compareVisibleText(vueText, reactText)
      const tokenDrops = [...diff.vueOnly, ...diff.reactOnly].filter((t) => expectTokens.includes(t))
      expect(tokenDrops, `slot token asymmetry vueOnly=${diff.vueOnly} reactOnly=${diff.reactOnly}`).toEqual([])
    })
  }

  // ---- A-narrow gate: independent Logo host height parity (DAY-ONE; self-証靶) ----
  // parity-logo is not a manifest entry — both harnesses special-case it and
  // render <Logo type="tvu" size={32}>. Per-side selectors differ:
  //   Vue   → BaseLogo <span class="logo"> carries data-manifest-id (inheritAttrs)
  //   React → wrapper <div data-manifest-id> wraps the <tvu-logo> CE host; the #5
  //           display:inline baseline gap lives on THAT host.
  test('A-narrow [Logo] host height parity @light', async ({ page }) => {
    await page.goto(`${VUE}/internal/render-harness/logo/parity-logo?theme=light`, { waitUntil: 'networkidle' })
    const vueEl = page.locator('[data-manifest-id="parity-logo"]').first()
    await vueEl.waitFor({ state: 'visible' })
    const vueH = (await vueEl.boundingBox())!.height

    await page.goto(`${REACT}/?manifestId=parity-logo&theme=light`, { waitUntil: 'networkidle' })
    const reactEl = page.locator('[data-manifest-id="parity-logo"] tvu-logo').first()
    await reactEl.waitFor({ state: 'attached' })
    await page.waitForTimeout(200)
    const reactH = (await reactEl.boundingBox())!.height

    // Representation-independent: both should be 1:1 (±1px). Rolling back
    // Logo.vue :host{display:inline-flex} → reactH≈35 vs vueH≈32 → FAIL (Task 7).
    expect(Math.abs(vueH - reactH), `Logo host height parity: vue=${vueH} react=${reactH}`).toBeLessThanOrEqual(1)
  })

  // ---- A-broad survey: computed-style diff, full risk set (NON-gating) ----
  for (const entry of riskEntries) {
    test(`A-broad survey [${entry.codeComponent}] ${entry.manifestId} @${entry.theme ?? 'dark'}`, async ({ page }, testInfo) => {
      await loadVue(page, entry)
      const vueActual = await collectActual(page, entry)
      await loadReact(page, entry)
      const reactActual = await collectActual(page, entry)
      const diffs = compareComputedStyle(vueActual as Record<string, unknown>, reactActual as Record<string, unknown>)
      await testInfo.attach('style-diffs', {
        body: JSON.stringify({ manifestId: entry.manifestId, diffs }, null, 2),
        contentType: 'application/json',
      })
      testInfo.annotations.push({ type: 'style-parity', description: `${entry.manifestId}: ${diffs.length} diffs` })
      // A-broad is a baseline survey — do NOT hard-assert (spec §4.2). Diffs feed Task 8 triage.
    })
  }
}
