import { describe, it, expect } from 'vitest'
import type { EntryReport, CheckResult } from './visual-verify/lib/drift-compare-core'
import {
  NAVIGATION_FIELD,
  collectClassifiedChecks,
  summarize,
} from './visual-verify/lib/render-report-summary'

/**
 * fixture builder —— 只填 summarize() 真正读的字段，其余用类型要求的最小值。
 * ⚠️ 这些 fixture 是**判据的测量对象**，不是判据本身。
 */
function entry(
  manifestId: string,
  status: EntryReport['status'],
  checks: CheckResult[],
): EntryReport {
  return {
    manifestId,
    figmaNodeId: '1:1',
    figmaName: 'Fixture',
    figmaVariantName: 'Fixture/variant',
    codeComponent: 'Fixture',
    codeProps: {},
    status,
    checks,
  }
}

function navFailCheck(): CheckResult {
  return {
    field: NAVIGATION_FIELD,
    expected: 'route loaded',
    actual: 'page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:5173/x',
    pass: false,
    status: 'fail',
  }
}

function realCheck(
  field: string,
  pass: boolean,
  classification?: CheckResult['classification'],
): CheckResult {
  return {
    field,
    expected: '10px',
    actual: pass ? '10px' : '12px',
    pass,
    status: pass ? 'pass' : 'fail',
    ...(classification ? { classification } : {}),
  }
}

describe('summarize', () => {
  it('健康态：每条 entry 都有真测量 check → navigationFailures=0, measuredEntries=total', () => {
    const reports = [
      entry('a', 'PASS', [realCheck('rootWidth', true), realCheck('rootHeight', true)]),
      entry('b', 'PASS', [realCheck('rootWidth', true)]),
      entry('c', 'FAIL', [realCheck('rootFillHex', false, 'A_TRUE_DRIFT_CANDIDATE')]),
    ]
    const s = summarize(reports, [])
    expect(s.total).toBe(3)
    expect(s.navigationFailures).toBe(0)
    expect(s.measuredEntries).toBe(3)
  })

  it('全空跑：每条 entry 只有一条导航失败 check → navigationFailures=N, measuredEntries=0', () => {
    const reports = [
      entry('a', 'FAIL', [navFailCheck()]),
      entry('b', 'FAIL', [navFailCheck()]),
      entry('c', 'FAIL', [navFailCheck()]),
    ]
    const s = summarize(reports, [])
    expect(s.total).toBe(3)
    expect(s.navigationFailures).toBe(3)
    expect(s.measuredEntries).toBe(0)
    // F97 本体：A 分类恒 0，所以旧闸看不见 —— 这里钉住那个事实
    expect(s.classifications.A_TRUE_DRIFT_CANDIDATE).toBe(0)
  })

  it('混合：两个数各自等于对应的 entry 数', () => {
    const reports = [
      entry('a', 'FAIL', [navFailCheck()]),
      entry('b', 'PASS', [realCheck('rootWidth', true)]),
      entry('c', 'FAIL', [navFailCheck()]),
      entry('d', 'FAIL', [realCheck('rootGap', false, 'B_RESIDUAL_SCHEMA_GAP')]),
    ]
    const s = summarize(reports, [])
    expect(s.total).toBe(4)
    expect(s.navigationFailures).toBe(2)
    expect(s.measuredEntries).toBe(2)
  })

  it('空输入：total=0 且不抛（passRate 分母保护）', () => {
    const s = summarize([], [])
    expect(s.total).toBe(0)
    expect(s.passRate).toBe(0)
    expect(s.navigationFailures).toBe(0)
    expect(s.measuredEntries).toBe(0)
    expect(s.classifications.A_TRUE_DRIFT_CANDIDATE).toBe(0)
  })

  it('现有六个字段与抽函数前逐字相同（算法回归）', () => {
    const reports = [
      entry('a', 'PASS', [realCheck('rootWidth', true)]),
      entry('b', 'PASS_BY_MODE_SKIP', [
        {
          field: 'rootFillHex',
          expected: null,
          actual: null,
          pass: true,
          status: 'pass-by-mode-skip',
          reason: 'mode skip',
        },
      ]),
      entry('c', 'FAIL', [
        realCheck('rootFillHex', false, 'A_TRUE_DRIFT_CANDIDATE'),
        realCheck('rootGap', false, 'B_RESIDUAL_SCHEMA_GAP'),
        realCheck('rootPaddingTop', false, 'C_BOUNDARY_CASE'),
      ]),
      entry('d', 'FAIL', [realCheck('rootHeight', false, 'C_BOUNDARY_CASE')]),
    ]
    const s = summarize(reports, [])
    expect(s.total).toBe(4)
    expect(s.pass).toBe(1)
    expect(s.passByModeSkip).toBe(1)
    expect(s.fail).toBe(2)
    expect(s.passRate).toBe(0.5) // (pass 1 + passByModeSkip 1) / 4
    expect(s.classifications).toEqual({
      A_TRUE_DRIFT_CANDIDATE: 1,
      B_RESIDUAL_SCHEMA_GAP: 1,
      C_BOUNDARY_CASE: 2,
    })
  })

  it('导航失败 check 若 pass=true 不计入 navigationFailures（判据是 field + !pass 两半）', () => {
    const reports = [
      entry('a', 'PASS', [
        {
          field: NAVIGATION_FIELD,
          expected: 'route loaded',
          actual: 'route loaded',
          pass: true,
          status: 'pass',
        },
        realCheck('rootWidth', true),
      ]),
    ]
    const s = summarize(reports, [])
    expect(s.navigationFailures).toBe(0)
    expect(s.measuredEntries).toBe(1)
  })

  it('同一 entry 多条导航失败 check 只算一个 entry（单位是 entry 不是 check）', () => {
    const reports = [entry('a', 'FAIL', [navFailCheck(), navFailCheck()])]
    const s = summarize(reports, [])
    expect(s.navigationFailures).toBe(1)
    expect(s.measuredEntries).toBe(0)
  })
})

describe('collectClassifiedChecks', () => {
  it('只收有 classification 的 check，并保留 entry 关联', () => {
    const reports = [
      entry('a', 'PASS', [realCheck('rootWidth', true)]),
      entry('b', 'FAIL', [
        realCheck('rootFillHex', false, 'A_TRUE_DRIFT_CANDIDATE'),
        realCheck('rootGap', true),
      ]),
    ]
    const pairs = collectClassifiedChecks(reports)
    expect(pairs).toHaveLength(1)
    expect(pairs[0].entry.manifestId).toBe('b')
    expect(pairs[0].check.classification).toBe('A_TRUE_DRIFT_CANDIDATE')
  })
})

describe('summarize — 豁免表统计（INFRA-F104）', () => {
  const rows = [
    { component: 'Fixture', field: 'rootWidth', entryScope: '*', reason: 'r', fixDirection: 'harness-verifier', addedAt: '2026-08-07', reviewBy: '2027-02-03' },
    { component: 'Fixture', field: 'rootHeight', entryScope: 'never-matches--*', reason: 'r', fixDirection: 'harness-verifier', addedAt: '2026-08-07', reviewBy: '2027-02-03' },
  ] as never

  const failed = (field: string): CheckResult => ({
    field, expected: 1, actual: 2, pass: false, status: 'fail', classification: 'B_RESIDUAL_SCHEMA_GAP',
  })

  it('命中的行计 matched，没命中的行进 unmatched（shrink-only 的测量口）', () => {
    const s = summarize([entry('e1', 'FAIL', [failed('rootWidth')])], rows)
    expect(s.excuse.rows).toBe(2)
    expect(s.excuse.matched).toBe(1)
    expect(s.excuse.unmatched).toEqual(['Fixture | rootHeight | never-matches--*'])
  })

  it('excusedChecks 数的是 check 不是 entry', () => {
    const s = summarize(
      [entry('e1', 'FAIL', [failed('rootWidth')]), entry('e2', 'FAIL', [failed('rootWidth')])],
      rows,
    )
    expect(s.excuse.excusedChecks).toBe(2)
  })

  // I6（review fix）：excuses 现在是必填参数（编译期消灭「漏传表」这条路径，S6b 从
  // 「唯一防线」退成双保险）。这条测试钉住的是「传空表」这个合法状态，不是「不传表」。
  it('传空表时 excuse 全零且 unmatched 为空', () => {
    const s = summarize([entry('e1', 'PASS', [])], [])
    expect(s.excuse).toEqual({ rows: 0, matched: 0, unmatched: [], excusedChecks: 0, rowScopes: [] })
  })
})

// I3（review fix）：entryScope 放大此前对 rows/matched/unmatched/excusedChecks 四个数字全部
// 不可见——放大只会命中今天已经 pass 的 entry，excusedChecks 只数 status==='fail' 的 check。
// rowScopes 按行记录 glob 的作用面，让放大在 report diff 里看得见。这条测试本身就是这个
// 「结构性不对称」的活文档：scopedEntries 会随放大变大，excusedChecks 纹丝不动。
describe('summarize — rowScopes（INFRA-F104 review I3）', () => {
  const failed = (field: string): CheckResult => ({
    field, expected: 1, actual: 2, pass: false, status: 'fail', classification: 'B_RESIDUAL_SCHEMA_GAP',
  })

  it('entryScope 放大后 scopedEntries 变大，但 excusedChecks 不变（不对称的活文档）', () => {
    const failingEntry = entry('e1', 'FAIL', [failed('rootWidth')])
    const passingEntry = entry('e2', 'PASS', [{ field: 'rootWidth', expected: 1, actual: 1, pass: true, status: 'pass' }])
    const narrow = [
      { component: 'Fixture', field: 'rootWidth', entryScope: 'e1', reason: 'r', fixDirection: 'harness-verifier', addedAt: '2026-08-07', reviewBy: '2027-02-03' },
    ] as never
    const wide = [
      { component: 'Fixture', field: 'rootWidth', entryScope: '*', reason: 'r', fixDirection: 'harness-verifier', addedAt: '2026-08-07', reviewBy: '2027-02-03' },
    ] as never

    const sNarrow = summarize([failingEntry, passingEntry], narrow)
    const sWide = summarize([failingEntry, passingEntry], wide)

    expect(sNarrow.excuse.rowScopes).toEqual([{ key: 'Fixture | rootWidth | e1', scopedEntries: 1, failingEntries: 1 }])
    expect(sWide.excuse.rowScopes).toEqual([{ key: 'Fixture | rootWidth | *', scopedEntries: 2, failingEntries: 1 }])
    // 放大只把一条今天 pass 的 entry 也纳入了 scope —— excusedChecks（只数 fail check）不变，
    // S5/S6/S6b/S7 与 report 的四个既有数字因此都看不见这次放大，rowScopes 是唯一能看见的地方。
    expect(sNarrow.excuse.excusedChecks).toBe(sWide.excuse.excusedChecks)
    expect(sNarrow.excuse.matched).toBe(sWide.excuse.matched)
  })

  it('key 与 unmatched 用完全相同的格式，且按 key 稳定排序', () => {
    const reports = [entry('z1', 'FAIL', [failed('rootHeight')]), entry('a1', 'FAIL', [failed('rootWidth')])]
    const rows = [
      { component: 'Zeta', field: 'rootHeight', entryScope: '*', reason: 'r', fixDirection: 'harness-verifier', addedAt: '2026-08-07', reviewBy: '2027-02-03' },
      { component: 'Alpha', field: 'rootWidth', entryScope: '*', reason: 'r', fixDirection: 'harness-verifier', addedAt: '2026-08-07', reviewBy: '2027-02-03' },
    ] as never
    // 两行故意乱序传入；rowScopes 必须按 key 排序输出，report diff 才可读。
    const s = summarize(reports, rows)
    expect(s.excuse.rowScopes.map((r) => r.key)).toEqual(['Alpha | rootWidth | *', 'Zeta | rootHeight | *'])
  })
})

// M8（review fix）：classifyFailedCheck 对 renderTarget / navigation 提前返回 B，根本不查表
// （它们是基础设施信号）。哪怕闭集允许这两个字段名出现在表里，excusedChecks 与 rowScopes
// 都不该把匹配它们的 check 算作「被豁免」——否则与 classifyFailedCheck 的真实行为口径不一。
describe('summarize — renderTarget / navigation 不经表（INFRA-F104 review M8）', () => {
  it('表里给 navigation 写一行也不会被计入 excusedChecks，且该行不出现在 rowScopes', () => {
    const navRow = [
      { component: 'Fixture', field: NAVIGATION_FIELD, entryScope: '*', reason: 'r', fixDirection: 'harness-verifier', addedAt: '2026-08-07', reviewBy: '2027-02-03' },
    ] as never
    const reports = [
      entry('e1', 'FAIL', [
        { field: NAVIGATION_FIELD, expected: 'route loaded', actual: 'timeout', pass: false, status: 'fail' },
      ]),
    ]
    const s = summarize(reports, navRow)
    expect(s.excuse.excusedChecks).toBe(0)
    expect(s.excuse.rowScopes).toEqual([])
  })

  it('renderTarget 同理不计入', () => {
    const renderTargetRow = [
      { component: 'Fixture', field: 'renderTarget', entryScope: '*', reason: 'r', fixDirection: 'harness-verifier', addedAt: '2026-08-07', reviewBy: '2027-02-03' },
    ] as never
    const reports = [
      entry('e1', 'FAIL', [
        { field: 'renderTarget', expected: 'visible', actual: 'missing: .x', pass: false, status: 'fail' },
      ]),
    ]
    const s = summarize(reports, renderTargetRow)
    expect(s.excuse.excusedChecks).toBe(0)
    expect(s.excuse.rowScopes).toEqual([])
  })
})
