import fs from 'node:fs'
import path from 'node:path'
import type { Page } from '@playwright/test'
import type { ManifestEntry, EntryReport } from './lib/drift-compare-core'
import { collectActual, collectNodeChecks, buildChecks } from './lib/drift-compare-core'

const manifestPath = path.resolve('figma-data/render-verification-manifest.json')
const entries = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as ManifestEntry[]
const reportJsonPath = path.resolve('figma-data/normalized/render-verification.report.json')
const reportMarkdownPath = path.resolve('docs/internal/render-verification-report.md')
const reports: EntryReport[] = []

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) => check.classification)
      .map((check) => ({ entry, check })),
  )
  const classificationCounts = {
    A_TRUE_DRIFT_CANDIDATE: classifiedChecks.filter(({ check }) => check.classification === 'A_TRUE_DRIFT_CANDIDATE').length,
    B_RESIDUAL_SCHEMA_GAP: classifiedChecks.filter(({ check }) => check.classification === 'B_RESIDUAL_SCHEMA_GAP').length,
    C_BOUNDARY_CASE: classifiedChecks.filter(({ check }) => check.classification === 'C_BOUNDARY_CASE').length,
  }
  // INFRA-F41 Task 3: node-coverage rollup, computed purely from per-entry
  // `nodeChecks`. It is REPORT-ONLY — it does NOT contribute to summary.classifications
  // (A/B/C) and cannot flip any entry status. In Task 3 (no `data-figma-node-id`
  // annotations yet) every expectedNode resolves to a coverage-gap, so compared=0,
  // mismatches=0, gaps=totalExpectedNodes.
  const allNodeChecks = reports.flatMap((entry) =>
    (entry.nodeChecks ?? []).map((nodeCheck) => ({ 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(),
    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 = [
    '# Render Verification Report',
    '',
    `> Generated: ${payload.checkedAt}`,
    `> Scope: manifest-based render verification`,
    '',
    `## 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 }) => check.classification === classification)
        .slice(0, 10)
      return [
        `### ${classification}`,
        '',
        samples.length
          ? '| Component | State | Field | Expected | Actual | Manifest ID |\n|---|---|---|---|---|---|\n'
            + samples.map(({ entry, check }) =>
              `| ${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) => !check.pass)
        .map((check) => `${check.classification ?? 'UNCLASSIFIED'} ${check.field}: expected ${check.expected}, actual ${check.actual}`)
        .join('<br>') || '-'
      const skippedChecks = entry.checks
        .filter((check) => check.status === 'pass-by-mode-skip')
        .map((check) => `${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`)
}

if (process.env.VITEST) {
  const { test } = await import('vitest')
  test.skip('Playwright render verification suite is run via playwright.render-verification.config.ts', () => {})
} else {
  const { test, expect } = await import('@playwright/test')

  test('Render manifest entries produce deterministic report', async ({ page }: { page: Page }) => {
    test.setTimeout(10 * 60 * 1000)
    for (const entry of entries) {
      try {
        await page.goto(entry.renderRoute, { 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) => !check.pass)
        ? 'FAIL'
        : checks.some((check) => 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)
  })
}
