import { describe, it, expect } from 'vitest'
import type { CheckResult, ExpectedFromFigma, ManifestEntry } from './visual-verify/lib/drift-compare-core'
import {
  CHECK_FIELDS,
  UNVERIFIABLE_STATUS,
  buildChecks,
  classifyFailedCheck,
  deriveEntryStatus,
  pushNumber,
  pushSizedNumber,
  pushUnverifiable,
} from './visual-verify/lib/drift-compare-core'
import { summarize } from './visual-verify/lib/render-report-summary'

/**
 * INFRA-F129 ① 治本项的**机制级**钉子 —— 「`expected` 缺失时必须发出一行，而不是静默丢弃」。
 *
 * 为什么这一层用单测而不是给闸加数字判据（判据选型的留痕）：
 *   要防的逃逸是**代码回归**（有人把 `if (expected === null) return` 改回去），而它在
 *   `report` 的数字上表现为「未验字段数掉到 0」—— 那与「真的修好了、Figma 补齐了值」在数字上
 *   **不可区分**。单测直接断言机制的终态行为，不依赖 chromium / dev server / report 新鲜度，
 *   且随 `pr-checks.yml` 的 vitest 跑（**L5**）。
 *   数字那一层的棘轮在 `pnpm audit:render-silent-checks`（per-component shrink-only）。
 *
 * 语义真源 = `tests/visual-verify/lib/drift-compare-core.ts` 的 `UNVERIFIABLE_STATUS` 头注释。
 */

/** `pushNumber` / `pushSizedNumber` 上 expected 可以为 null 的**全部**字段（2026-08-21 实测）。 */
const NULLABLE_FIELDS = ['rootRadius', 'rootWidth', 'rootHeight', 'textInset.left', 'textInset.right'] as const

function baseExpected(overrides: Partial<ExpectedFromFigma> = {}): ExpectedFromFigma {
  return {
    rootWidth: 100,
    rootWidthMode: 'FIXED',
    rootHeight: 40,
    rootHeightMode: 'FIXED',
    rootPadding: { top: 4, right: 8, bottom: 4, left: 8 },
    rootRadius: 4,
    rootGap: 8,
    rootGapMeaningful: true,
    rootOpacity: 1,
    rootFillHex: '#ffffff',
    rootFillCssVar: '--color-white',
    rootBorderHex: null,
    rootBorderCssVar: null,
    rootBorderWidth: 0,
    textFillHex: null,
    textFillCssVar: null,
    ...overrides,
  }
}

function baseEntry(overrides: Partial<ManifestEntry> = {}): ManifestEntry {
  return {
    manifestId: 'fixture--1-1',
    figmaNodeId: '1:1',
    figmaName: 'Fixture',
    figmaVariantName: 'Fixture/variant',
    codeComponent: 'Fixture',
    codeProps: {},
    renderRoute: '/internal/render-harness/fixture',
    expectedFromFigma: baseExpected(),
    ...overrides,
  }
}

/** `collectActual()` 的返回形状（buildChecks 只读这些键）。 */
function baseActual(overrides: Record<string, unknown> = {}) {
  return {
    width: 100,
    height: 40,
    paddingTop: '4px',
    paddingRight: '8px',
    paddingBottom: '4px',
    paddingLeft: '8px',
    borderRadius: '4px',
    gap: '8px',
    opacity: '1',
    color: 'rgb(20, 20, 20)',
    backgroundColor: 'rgb(255, 255, 255)',
    borderColor: 'rgb(20, 20, 20)',
    borderWidth: '0px',
    ...overrides,
  } as unknown as Awaited<ReturnType<typeof import('./visual-verify/lib/drift-compare-core').collectActual>>
}

describe('INFRA-F129 ① pushNumber / pushSizedNumber 不再静默丢行', () => {
  it('pushNumber(expected=null) 发出一行 unverifiable，而不是 return', () => {
    const checks: CheckResult[] = []
    pushNumber(checks, 'rootRadius', 10, null)
    expect(checks).toHaveLength(1)
    expect(checks[0]).toMatchObject({
      field: 'rootRadius',
      actual: 10,
      expected: null,
      pass: true,
      status: UNVERIFIABLE_STATUS,
    })
    expect(checks[0].reason).toBeTruthy()
  })

  it('expected 照实留 null —— ⛔ 不补 0（那是把「Figma 没说」写成「Figma 说是 0」）', () => {
    const checks: CheckResult[] = []
    pushNumber(checks, 'rootRadius', 10, null)
    expect(checks[0].expected).toBeNull()
    expect(checks[0].expected).not.toBe(0)
  })

  it('actual 照实记 —— 这是仓库里唯一一份「Figma 没说时 code 渲成什么」的记录', () => {
    const checks: CheckResult[] = []
    pushNumber(checks, 'rootRadius', 10, null)
    pushNumber(checks, 'rootRadius', 0, null)
    expect(checks.map((c) => c.actual)).toEqual([10, 0])
  })

  it('pushSizedNumber(expected=null) 三种 mode 都走同一条 unverifiable 路（判据只写一遍）', () => {
    for (const mode of ['FIXED', 'HUG', 'FILL'] as const) {
      const checks: CheckResult[] = []
      pushSizedNumber(checks, 'rootWidth', 123, null, mode)
      expect(checks, `mode=${mode}`).toHaveLength(1)
      expect(checks[0].status, `mode=${mode}`).toBe(UNVERIFIABLE_STATUS)
      // ⚠️ null 分支必须**先于** mode 分支：HUG 会发 pass-by-mode-skip，那正是本判据
      // 拒绝复用的那一档（它会进 passRate 分子）。
      expect(checks[0].status, `mode=${mode}`).not.toBe('pass-by-mode-skip')
    }
  })

  it('expected 有值时行为逐字不变（阴性对照 —— 证明不是把所有行都改成 unverifiable）', () => {
    const checks: CheckResult[] = []
    pushNumber(checks, 'rootRadius', 4, 4)
    pushNumber(checks, 'rootRadius', 9, 4)
    pushSizedNumber(checks, 'rootWidth', 200, 100, 'HUG')
    expect(checks.map((c) => c.status)).toEqual(['pass', 'fail', 'pass-by-mode-skip'])
    expect(checks.some((c) => c.status === UNVERIFIABLE_STATUS)).toBe(false)
  })
})

describe('INFRA-F129 ① buildChecks：每个可 null 字段都不会静默消失', () => {
  it('rootRadius = null → 行在，且标 unverifiable（这是今日唯一真在发生的那个字段）', () => {
    const entry = baseEntry({ expectedFromFigma: baseExpected({ rootRadius: null }) })
    const checks = buildChecks(entry, baseActual({ borderRadius: '4px' }), entry.expectedFromFigma)
    const row = checks.find((c) => c.field === 'rootRadius')
    expect(row).toBeDefined()
    expect(row!.status).toBe(UNVERIFIABLE_STATUS)
    expect(row!.actual).toBe(4)
  })

  it('五个可 null 字段全为 null 时，五行全部发出（潜伏路径也覆盖）', () => {
    const entry = baseEntry({
      textInset: { left: null, right: null },
      expectedFromFigma: baseExpected({ rootRadius: null, rootWidth: null, rootHeight: null }),
    })
    const checks = buildChecks(entry, baseActual(), entry.expectedFromFigma)
    const unverifiableFields = checks
      .filter((c) => c.status === UNVERIFIABLE_STATUS)
      .map((c) => c.field)
      .sort()
    expect(unverifiableFields).toEqual([...NULLABLE_FIELDS].sort())
  })

  it('每个 unverifiable 行的 field 都在 CHECK_FIELDS 闭集里（否则永远进不了豁免表 / 统计）', () => {
    const entry = baseEntry({
      textInset: { left: null, right: null },
      expectedFromFigma: baseExpected({ rootRadius: null, rootWidth: null, rootHeight: null }),
    })
    const checks = buildChecks(entry, baseActual(), entry.expectedFromFigma)
    for (const check of checks) {
      expect(CHECK_FIELDS as readonly string[]).toContain(check.field)
    }
  })

  it('unverifiable 不进 A/B/C 分类（classifyFailedCheck 首行 pass 短路）', () => {
    const entry = baseEntry({ expectedFromFigma: baseExpected({ rootRadius: null }) })
    const checks = buildChecks(entry, baseActual(), entry.expectedFromFigma)
    const row = checks.find((c) => c.field === 'rootRadius')!
    expect(row.classification).toBeUndefined()
    expect(classifyFailedCheck(entry, row, [])).toBeUndefined()
  })
})

describe('INFRA-F129 ① deriveEntryStatus：entry 级四个数逐字不变', () => {
  const unverifiableRow: CheckResult = {
    field: 'rootRadius',
    expected: null,
    actual: 4,
    pass: true,
    status: UNVERIFIABLE_STATUS,
  }
  const passRow: CheckResult = { field: 'rootGap', expected: 8, actual: 8, pass: true, status: 'pass' }
  const skipRow: CheckResult = { field: 'rootWidth', expected: 100, actual: 120, pass: true, status: 'pass-by-mode-skip' }
  const failRow: CheckResult = { field: 'rootFillHex', expected: '#fff', actual: '#000', pass: false, status: 'fail' }

  it('全 pass + 一条 unverifiable → 仍是 PASS（⛔ 不是新状态，刻意如此）', () => {
    expect(deriveEntryStatus([passRow, unverifiableRow])).toBe('PASS')
  })

  it('有 skip + 一条 unverifiable → 仍是 PASS_BY_MODE_SKIP', () => {
    expect(deriveEntryStatus([passRow, skipRow, unverifiableRow])).toBe('PASS_BY_MODE_SKIP')
  })

  it('有 fail → FAIL（unverifiable 不遮盖真失败）', () => {
    expect(deriveEntryStatus([failRow, unverifiableRow])).toBe('FAIL')
  })

  it('只有 unverifiable 一行 → PASS（因为它 pass:true；⚠️ 「pass」在这里只表示不判红）', () => {
    expect(deriveEntryStatus([unverifiableRow])).toBe('PASS')
  })
})

describe('INFRA-F129 ① summarize：unverifiable 是第三个桶，不进 pass/fail/passRate', () => {
  function entryReport(manifestId: string, codeComponent: string, checks: CheckResult[]) {
    return {
      manifestId,
      figmaNodeId: '1:1',
      figmaName: 'Fixture',
      figmaVariantName: 'v',
      codeComponent,
      codeProps: {},
      status: deriveEntryStatus(checks),
      checks,
    }
  }
  const pass: CheckResult = { field: 'rootGap', expected: 8, actual: 8, pass: true, status: 'pass' }
  const unv = (field: string, actual: number): CheckResult => ({
    field,
    expected: null,
    actual,
    pass: true,
    status: UNVERIFIABLE_STATUS,
  })

  it('计数 + byField + byComponent（key 用 codeComponent，与 audit:render-silent-checks 同口径）', () => {
    const summary = summarize(
      [
        entryReport('a', 'Switch', [pass, unv('rootRadius', 10)]),
        entryReport('b', 'Switch', [pass, unv('rootRadius', 10)]),
        entryReport('c', 'Tooltip', [pass, unv('rootRadius', 4), unv('rootWidth', 88)]),
      ],
      [],
    )
    expect(summary.unverifiableChecks.total).toBe(4)
    expect(summary.unverifiableChecks.byField).toEqual({ rootRadius: 3, rootWidth: 1 })
    expect(summary.unverifiableChecks.byComponent).toEqual({ Switch: 2, Tooltip: 2 })
  })

  it('⛔ 不污染 pass / passByModeSkip / fail / passRate（本判据最核心的一条）', () => {
    const withoutUnv = summarize([entryReport('a', 'Switch', [pass])], [])
    const withUnv = summarize([entryReport('a', 'Switch', [pass, unv('rootRadius', 10)])], [])
    expect(withUnv.pass).toBe(withoutUnv.pass)
    expect(withUnv.passByModeSkip).toBe(withoutUnv.passByModeSkip)
    expect(withUnv.fail).toBe(withoutUnv.fail)
    expect(withUnv.passRate).toBe(withoutUnv.passRate)
    expect(withUnv.classifications).toEqual(withoutUnv.classifications)
    // 唯一该变的那个数：
    expect(withoutUnv.unverifiableChecks.total).toBe(0)
    expect(withUnv.unverifiableChecks.total).toBe(1)
  })

  it('unverifiable 永远进不了豁免表统计（excusedChecks 只数 status === "fail"）', () => {
    const summary = summarize([entryReport('a', 'Switch', [unv('rootRadius', 10)])], [
      {
        component: 'Fixture',
        field: 'rootRadius',
        entryScope: '*',
        reason: '故意放一行能匹配到的豁免 —— 它不该命中 unverifiable',
        fixDirection: 'extract-pipeline',
        addedAt: '2026-08-21',
        reviewBy: '2026-10-01',
        ceReviewedAt: '2026-08-21',
        ceNote: 'fixture',
      },
    ])
    expect(summary.excuse.excusedChecks).toBe(0)
    expect(summary.excuse.matched).toBe(0)
  })
})
