// INFRA-F97: render-verification report 的统计层（从 manifest-verifier.spec.ts 的
// writeReports() 内联统计抽出，便于单测 + 让 `audit:render-drift-gate` 有「测量量」可读）。
//
// 为什么测量量在这里算、闸只读数字（spec §3）：
//   'navigation' 这个字面量只在**产生**那条 check 的地方出现（manifest-verifier.spec.ts
//   的 catch 块），它与这里的统计共用同一个 NAVIGATION_FIELD 常量 —— 全仓一处。闸压根
//   不需要知道「什么算基础设施失败」，只做数字比较，与它现在读 summary.classifications
//   （不自己分类）是同一形态。
//   ⛔ 别改成让闸自己走 report.entries 派生：那会让判据在两个文件各写一遍，将来加第二种
//   基础设施失败（如 selector 解析不到）时闸会把它当成真测量 → 静默失效。
//
// ⚠️ summarize() 刻意不接 manifestLength —— S2（report 条数 === 磁盘 manifest 条数）必须
//   由闸读磁盘来做。spec 侧已有 expect(reports).toHaveLength(entries.length)，在这里再比
//   一次是空过（同一次运行内两者恒等）；S2 的价值在**跨运行**比对。
//
// 判据真源：docs/superpowers/specs/2026-08-05-infra-f97-render-gate-measurement-floor-design.md
import type { EntryReport, CheckResult } from './drift-compare-core'
import { UNVERIFIABLE_STATUS } from './drift-compare-core'
import { globToRegExp, isExcused, type ExcuseRow } from './render-excused-fields'

/**
 * 导航失败 check 的 field 名。**全仓唯一定义** —— 产生该 check 的
 * manifest-verifier.spec.ts catch 块与本文件的统计共用它。
 */
export const NAVIGATION_FIELD = 'navigation'

export type RenderReportSummary = {
  total: number
  pass: number
  passByModeSkip: number
  fail: number
  passRate: number
  classifications: {
    A_TRUE_DRIFT_CANDIDATE: number
    B_RESIDUAL_SCHEMA_GAP: number
    C_BOUNDARY_CASE: number
  }
  /** 至少有一条 `field === NAVIGATION_FIELD && !pass` 的 check 的 **entry 数** */
  navigationFailures: number
  /** 至少有一条 `field !== NAVIGATION_FIELD` 的 check 的 **entry 数** */
  measuredEntries: number
  /**
   * INFRA-F129 ①（2026-08-21）：**check 级第三个桶** —— 「Figma 侧没有可比的值」的 check 数。
   *
   * 它与上面 `pass` / `passByModeSkip` / `fail` **不是同一个单位**（那三个数 entry 级、
   * 本字段 check 级），也**刻意不参与 `passRate`**。为什么是「另立一栏」而不是「拉低 pass 率」，
   * 唯一真源 = `drift-compare-core.ts` 的 `UNVERIFIABLE_STATUS` 头注释 §pass 率怎么处理。
   *
   * `byField` 的存在理由：静默路径不只 `rootRadius` —— `pushNumber` / `pushSizedNumber` 上
   * 共有 5 个字段的 expected 可以是 null（`rootRadius` / `rootWidth` / `rootHeight` /
   * `textInset.left` / `textInset.right`），2026-08-21 实测只有 `rootRadius` 非 0，其余四个是
   * **潜伏路径**。⇒ 这个键集变宽本身就是信号，别只看 total。
   *
   * ⚠️ 判据强度如实登记：本字段是**可见性**，不是闸。它变大不会让任何闸红（那会与
   * `audit:render-silent-checks` 的 per-component 棘轮撞成「同一件事红两次」）；闸侧只有
   * **S10 形状 fail-closed**，防「旧格式 report 冒充 0 条未验」。
   */
  unverifiableChecks: {
    total: number
    byField: Record<string, number>
    byComponent: Record<string, number>
  }
  /**
   * INFRA-F104 豁免表的测量口。判据在闸侧（S6 要求 unmatched 为空 = shrink-only），
   * 但**统计在这里算** —— 与 navigationFailures / measuredEntries 同一形态：
   * 闸只做数字比较，不需要知道「什么算命中」。
   */
  excuse: {
    rows: number
    matched: number
    unmatched: string[]
    excusedChecks: number
    /**
     * INFRA-F104 review I3：`entryScope` 放大（例如把 `*--theme-light` 改宽成 `*`）此前对
     * rows/matched/unmatched/excusedChecks 四个数字全部不可见——放大只会命中今天已经 pass
     * 的 entry，`excusedChecks` 只数 `status === 'fail'` 的 check，所以不动。这里按行记录
     * glob 的作用面，让任何放大都在**已提交进 git 的报告 diff**里看得见。
     * ⚠️ 这只是记录，不是判据——不要给它加阈值闸：它会随 manifest 正常扩容而增长，
     * 阈值必然误报（同 spec §8 被否掉的路线 C 的理由）。
     * `key` 与 `unmatched` 用完全相同的格式：`${component} | ${field} | ${entryScope}`。
     */
    rowScopes: Array<{ key: string; scopedEntries: number; failingEntries: number }>
  }
}

/**
 * 已分类 check 与其所属 entry 的配对。计数与 markdown 取样共用同一个定义，
 * 避免「什么算已分类 check」在两个文件各写一遍。
 */
export function collectClassifiedChecks(
  reports: EntryReport[],
): { entry: EntryReport; check: CheckResult }[] {
  return reports.flatMap((entry) =>
    entry.checks
      .filter((check) => check.classification)
      .map((check) => ({ entry, check })),
  )
}

// M8（review fix）：classifyFailedCheck 在这两个字段上提前返回 B_RESIDUAL_SCHEMA_GAP，
// **根本不查豁免表**（drift-compare-core.ts :440 附近）——它们是基础设施信号，不是视觉判据。
// 闭集允许这两个名字出现在表的 `field` 里（buildChecks 确实会产出它们），但如果真有人
// 加了这样一行，excusedChecks / rowScopes 不该把它算作「被豁免」，否则与 classifyFailedCheck
// 的真实行为口径不一。
const INFRASTRUCTURE_FIELDS: ReadonlySet<string> = new Set(['renderTarget', NAVIGATION_FIELD])

/**
 * INFRA-F129 ①：check 级第三个桶的统计。⛔ 与 pass/fail 三档**互斥且不重叠** ——
 * `unverifiable` 的 check 既不进 `pass`（那是 entry 级、且 `status === 'pass'` 才算验过）
 * 也不进 `fail`。语义真源见 `UNVERIFIABLE_STATUS`。
 */
function countUnverifiable(reports: EntryReport[]): RenderReportSummary['unverifiableChecks'] {
  const byField: Record<string, number> = {}
  const byComponent: Record<string, number> = {}
  let total = 0
  for (const entry of reports) {
    for (const check of entry.checks) {
      if (check.status !== UNVERIFIABLE_STATUS) continue
      total++
      byField[check.field] = (byField[check.field] ?? 0) + 1
      // key 用 `codeComponent` 而不是 `figmaName` —— 二者在本仓会分叉（`Switch`↔`switch` ·
      // `Tooltip`↔`Tooltips` · `Button`↔`Button/url link`），且 `audit:render-silent-checks`
      // 的 baseline 用的就是 `codeComponent`，两处必须对得上才能互相核。
      byComponent[entry.codeComponent] = (byComponent[entry.codeComponent] ?? 0) + 1
    }
  }
  const sortKeys = (o: Record<string, number>) =>
    Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)))
  return { total, byField: sortKeys(byField), byComponent: sortKeys(byComponent) }
}

export function summarize(reports: EntryReport[], excuses: readonly ExcuseRow[]): RenderReportSummary {
  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 = collectClassifiedChecks(reports)
  // INFRA-F104: excusedChecks 数的是**check**（分母是「有多少条 fail check 被免疫」），
  // matched/unmatched 数的是**行**（分母是「表里 37 行各自有没有打中过至少一条」）——
  // 两个视角故意不同单位，别互相替代。
  const excusedChecks = reports.reduce(
    (n, entry) =>
      n +
      entry.checks.filter(
        (check) =>
          check.status === 'fail' &&
          !INFRASTRUCTURE_FIELDS.has(check.field) &&
          isExcused(excuses, entry.figmaName, entry.manifestId, check.field),
      ).length,
    0,
  )
  const matchedRows = excuses.filter((row) =>
    reports.some(
      (entry) =>
        entry.figmaName === row.component &&
        globToRegExp(row.entryScope).test(entry.manifestId) &&
        entry.checks.some((check) => check.field === row.field && check.status === 'fail'),
    ),
  )
  // I3（review fix）：按行记录 entryScope 的作用面，见类型定义处的注释。
  const rowScopes = excuses
    .filter((row) => !INFRASTRUCTURE_FIELDS.has(row.field))
    .map((row) => {
      const scopedEntries = reports.filter(
        (entry) => entry.figmaName === row.component && globToRegExp(row.entryScope).test(entry.manifestId),
      )
      const failingEntries = scopedEntries.filter((entry) =>
        entry.checks.some((check) => check.field === row.field && check.status === 'fail'),
      )
      return {
        key: `${row.component} | ${row.field} | ${row.entryScope}`,
        scopedEntries: scopedEntries.length,
        failingEntries: failingEntries.length,
      }
    })
    .sort((a, b) => a.key.localeCompare(b.key))
  return {
    total: reports.length,
    pass,
    passByModeSkip,
    fail,
    passRate: reports.length ? (pass + passByModeSkip) / reports.length : 0,
    classifications: {
      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,
    },
    navigationFailures: reports.filter((entry) =>
      entry.checks.some((check) => check.field === NAVIGATION_FIELD && !check.pass),
    ).length,
    measuredEntries: reports.filter((entry) =>
      entry.checks.some((check) => check.field !== NAVIGATION_FIELD),
    ).length,
    unverifiableChecks: countUnverifiable(reports),
    excuse: {
      rows: excuses.length,
      matched: matchedRows.length,
      unmatched: excuses
        .filter((row) => !matchedRows.includes(row))
        .map((row) => `${row.component} | ${row.field} | ${row.entryScope}`),
      excusedChecks,
      rowScopes,
    },
  }
}
