// Full 180-entry React render-verification gate.
// Iterates all Button (4) + InputBoxLine (112) + FormItem (64) manifest entries.
// Navigates each to /?manifestId=...&theme=..., reads getComputedStyle via
// collectActual, runs buildChecks against expectedFromFigma, and writes a full
// report to figma-data/normalized/react-render-verification.report.json and
// docs/internal/react-render-verification-report.md.
//
// Guard: vitest picks up *.spec.ts — skip gracefully so pre-commit vitest run
// doesn't try to run Playwright test() in jsdom context.
if (process.env.VITEST) {
  const { test } = await import('vitest')
  test.skip('React render-verification full gate is run via playwright.render-verification-react.config.ts', () => {})
} else {
  const { test, expect } = await import('@playwright/test')
  const fs = await import('node:fs')
  const path = await import('node:path')
  const {
    collectActual,
    buildChecks,
    collectNodeChecks,
  } = await import('./lib/drift-compare')

  const manifestPath = path.resolve('figma-data/render-verification-manifest.json')
  const allEntries = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
  const entries = allEntries.filter((e: any) =>
    ['Button', 'InputBoxLine', 'FormItem', 'Badge', 'PillStatus', 'Progress', 'Rating', 'BreadcrumbItem', 'TopBar', 'Switch', 'CheckBox', 'Radio', 'InputBoxFilled', 'InputNumber', 'Slider', 'Pagination', 'TabList', 'TabItem', 'StepItem', 'PopupBox', 'SelectBoxLine', 'SelectBoxFilled', 'DropDownListSelect', 'Tooltip', 'Notification', 'Message', 'Table', 'Chart'].includes(e.codeComponent)
  )
  const reportJsonPath = path.resolve('figma-data/normalized/react-render-verification.report.json')
  const reportMarkdownPath = path.resolve('docs/internal/react-render-verification-report.md')
  const reports: any[] = []

  function writeReports() {
    const pass = reports.filter((entry) => entry.status === 'PASS').length
    const passByModeSkip = reports.filter((entry) => entry.status === 'PASS_BY_MODE_SKIP').length
    const fail = reports.filter((entry) => entry.status === 'FAIL').length
    const classifiedChecks = reports.flatMap((entry) =>
      entry.checks
        .filter((check: any) => check.classification)
        .map((check: any) => ({ entry, check })),
    )
    const classificationCounts = {
      A_TRUE_DRIFT_CANDIDATE: classifiedChecks.filter(({ check }: any) => check.classification === 'A_TRUE_DRIFT_CANDIDATE').length,
      B_RESIDUAL_SCHEMA_GAP: classifiedChecks.filter(({ check }: any) => check.classification === 'B_RESIDUAL_SCHEMA_GAP').length,
      C_BOUNDARY_CASE: classifiedChecks.filter(({ check }: any) => check.classification === 'C_BOUNDARY_CASE').length,
    }
    const allNodeChecks = reports.flatMap((entry) =>
      (entry.nodeChecks ?? []).map((nodeCheck: any) => ({ entry, nodeCheck })),
    )
    type NodeCoverageBucket = { compared: number; passed: number; mismatches: number; gaps: number }
    const newBucket = (): NodeCoverageBucket => ({ compared: 0, passed: 0, mismatches: 0, gaps: 0 })
    const byComponent: Record<string, NodeCoverageBucket> = {}
    const totalBucket = newBucket()
    for (const { entry, nodeCheck } of allNodeChecks) {
      const bucket = (byComponent[entry.figmaName] ??= newBucket())
      if (nodeCheck.status === 'pass') {
        totalBucket.compared++, totalBucket.passed++
        bucket.compared++, bucket.passed++
      } else if (nodeCheck.status === 'mismatch') {
        totalBucket.compared++, totalBucket.mismatches++
        bucket.compared++, bucket.mismatches++
      } else {
        totalBucket.gaps++
        bucket.gaps++
      }
    }
    const nodeCoverage = {
      totalExpectedNodes: allNodeChecks.length,
      compared: totalBucket.compared,
      passed: totalBucket.passed,
      mismatches: totalBucket.mismatches,
      gaps: totalBucket.gaps,
      byComponent,
    }
    const payload = {
      checkedAt: new Date().toISOString(),
      gate: 'react-pilot',
      summary: {
        total: reports.length,
        pass,
        passByModeSkip,
        fail,
        passRate: reports.length ? (pass + passByModeSkip) / reports.length : 0,
        classifications: classificationCounts,
      },
      nodeCoverage,
      entries: reports,
    }
    fs.mkdirSync(path.dirname(reportJsonPath), { recursive: true })
    fs.writeFileSync(reportJsonPath, `${JSON.stringify(payload, null, 2)}\n`)

    const lines = [
      '# React Render Verification Report',
      '',
      `> Generated: ${payload.checkedAt}`,
      '> Gate: React pilot (getComputedStyle vs Figma expected)',
      '> Scope: Button (4) + InputBoxLine (112) + FormItem (64) = 180 entries',
      '',
      '## Summary',
      '',
      '| Total | PASS | PASS_BY_MODE_SKIP | FAIL | Pass rate |',
      '|---:|---:|---:|---:|---:|',
      `| ${payload.summary.total} | ${pass} | ${passByModeSkip} | ${fail} | ${(payload.summary.passRate * 100).toFixed(1)}% |`,
      '',
      '| A true drift candidates | B residual schema gaps | C boundary cases |',
      '|---:|---:|---:|',
      `| ${payload.summary.classifications.A_TRUE_DRIFT_CANDIDATE} | ${payload.summary.classifications.B_RESIDUAL_SCHEMA_GAP} | ${payload.summary.classifications.C_BOUNDARY_CASE} |`,
      '',
      '## Node Coverage',
      '',
      '> INFRA-F41 Task 3: isolated node-walk pass. REPORT-ONLY — does not affect A/B/C or entry status. Coverage-gaps are informational (no `data-figma-node-id` annotations until Task 4).',
      '',
      '| Total expected nodes | Compared | Passed | Mismatches | Gaps |',
      '|---:|---:|---:|---:|---:|',
      `| ${nodeCoverage.totalExpectedNodes} | ${nodeCoverage.compared} | ${nodeCoverage.passed} | ${nodeCoverage.mismatches} | ${nodeCoverage.gaps} |`,
      '',
      '## Classification Samples',
      '',
      ...(['A_TRUE_DRIFT_CANDIDATE', 'B_RESIDUAL_SCHEMA_GAP', 'C_BOUNDARY_CASE'] as const).flatMap((classification) => {
        const samples = classifiedChecks
          .filter(({ check }: any) => check.classification === classification)
          .slice(0, 10)
        return [
          `### ${classification}`,
          '',
          samples.length
            ? '| Component | State | Field | Expected | Actual | Manifest ID |\n|---|---|---|---|---|---|\n'
              + samples
                .map(
                  ({ entry, check }: any) =>
                    `| ${entry.figmaName} | ${entry.state ?? '-'} | ${check.field} | ${check.expected ?? '-'} | ${check.actual ?? '-'} | \`${entry.manifestId}\` |`,
                )
                .join('\n')
            : '_No samples._',
          '',
        ]
      }),
      '## Entries',
      '',
      '| Status | Component | Manifest ID | Figma variant | Failed checks |',
      '|---|---|---|---|---|',
      ...reports.map((entry) => {
        const failedChecks =
          entry.checks
            .filter((check: any) => !check.pass)
            .map(
              (check: any) =>
                `${check.classification ?? 'UNCLASSIFIED'} ${check.field}: expected ${check.expected}, actual ${check.actual}`,
            )
            .join('<br>') || '-'
        const skippedChecks =
          entry.checks
            .filter((check: any) => check.status === 'pass-by-mode-skip')
            .map((check: any) => `${check.field}: ${check.reason}`)
            .join('<br>') || '-'
        const figmaVariant = entry.theme
          ? `${entry.figmaVariantName} (${entry.theme})`
          : entry.figmaVariantName
        return `| ${entry.status} | ${entry.figmaName} | \`${entry.manifestId}\` | ${figmaVariant} | ${failedChecks}${skippedChecks === '-' ? '' : `<br><strong>Skipped:</strong><br>${skippedChecks}`} |`
      }),
      '',
    ]
    fs.mkdirSync(path.dirname(reportMarkdownPath), { recursive: true })
    fs.writeFileSync(reportMarkdownPath, `${lines.join('\n')}\n`)
  }

  test('React render manifest entries produce deterministic report', async ({ page }) => {
    test.setTimeout(10 * 60 * 1000)
    for (const entry of entries) {
      try {
        await page.goto(
          `/?manifestId=${encodeURIComponent(entry.manifestId)}&theme=${entry.theme ?? 'dark'}`,
          { waitUntil: 'domcontentloaded', timeout: 15_000 },
        )
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error)
        reports.push({
          manifestId: entry.manifestId,
          figmaNodeId: entry.figmaNodeId,
          figmaName: entry.figmaName,
          figmaVariantName: entry.figmaVariantName,
          theme: entry.theme,
          state: entry.state,
          warnings: entry._warnings,
          codeComponent: entry.codeComponent,
          codeProps: entry.codeProps,
          status: 'FAIL',
          checks: [
            {
              field: 'navigation',
              actual: message.split('\n')[0],
              expected: 'route loaded',
              pass: false,
              status: 'fail',
            },
          ],
        })
        continue
      }
      const actual = await collectActual(page, entry)
      const checks = buildChecks(entry, actual, entry.expectedFromFigma)
      const status = checks.some((check: any) => !check.pass)
        ? 'FAIL'
        : checks.some((check: any) => check.status === 'pass-by-mode-skip')
          ? 'PASS_BY_MODE_SKIP'
          : 'PASS'
      // INFRA-F41 Task 3: isolated node-walk on the SAME loaded page. Deliberately
      // computed AFTER `status` so it can never feed into the status expression
      // above, and stored as a SEPARATE `nodeChecks` field (NOT merged into `checks`).
      const nodeChecks = await collectNodeChecks(page, entry)
      reports.push({
        manifestId: entry.manifestId,
        figmaNodeId: entry.figmaNodeId,
        figmaName: entry.figmaName,
        figmaVariantName: entry.figmaVariantName,
        theme: entry.theme,
        state: entry.state,
        warnings: entry._warnings,
        codeComponent: entry.codeComponent,
        codeProps: entry.codeProps,
        status,
        checks,
        nodeChecks,
      })
    }

    writeReports()
    expect(reports).toHaveLength(entries.length)
    const trueDrift = reports.flatMap((r) => r.checks ?? []).filter((c: any) => c.classification === 'A_TRUE_DRIFT_CANDIDATE')
    expect(trueDrift, `A_TRUE_DRIFT_CANDIDATE checks must be 0, found ${trueDrift.length}: ${trueDrift.map((c: any) => c.field).join(', ')}`).toHaveLength(0)
  })
}
