import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
  bytes,
  sliceStatus,
  STATUS_FILE,
  findCompletedEntries,
  evaluate,
  TOP_MAX,
  ACTIVE_LINE_MAX,
  EXEMPTIONS,
} from '../scripts/audit-doc-shape.mjs'

const REPO_ROOT = resolve(__dirname, '..')
const realStatus = () => readFileSync(resolve(REPO_ROOT, STATUS_FILE), 'utf-8')

const SAMPLE = [
  '# T',
  '',
  '> **Last updated**: 摘要',
  '>',
  '> ### 一 · 等 owner 拍板',
  '> 1. ~~已完成~~ —— ✅ 收口',
  '> 2. 还没做',
  '',
  '---',
  '',
  '## Active 后续工作',
  '',
  '```',
  'Backlog (active)  (2):  A·B',
  'Deferred (1): x',
  '```',
  '',
].join('\n')

describe('bytes', () => {
  it('按 UTF-8 字节算，不是 JS 字符串长度', () => {
    expect(bytes('中')).toBe(3)
    expect('中'.length).toBe(1) // 钉住这个陷阱：用 .length 会少算 2/3
  })
})

describe('sliceStatus', () => {
  it('顶部摘要区 = 文件头到首个 "> ### " 之前', () => {
    const s = sliceStatus(SAMPLE)
    expect(s.top).toContain('Last updated')
    expect(s.top).not.toContain('等 owner 拍板')
  })

  it('open 清单区 = 首个 "> ### " 到首个 "---"', () => {
    const s = sliceStatus(SAMPLE)
    expect(s.openList).toContain('等 owner 拍板')
    expect(s.openList).toContain('还没做')
    expect(s.openList).not.toContain('Active 后续工作')
  })

  it('Active fence 行带 label 与字节数', () => {
    const s = sliceStatus(SAMPLE)
    expect(s.activeLines!.map((l) => l.label)).toEqual(['Backlog (active)', 'Deferred'])
    expect(s.activeLines![0].bytes).toBe(bytes('Backlog (active)  (2):  A·B'))
  })

  it('真仓库三段都非空（分母回归钉）', () => {
    const s = sliceStatus(realStatus())
    expect(bytes(s.top)).toBeGreaterThan(0)
    expect(bytes(s.openList)).toBeGreaterThan(0)
    expect(s.activeLines!.length).toBeGreaterThan(0)
  })
})

const slicesOf = (over = {}) => ({
  top: 'x'.repeat(TOP_MAX - 100),
  openList: '> ### 一\n> 1. 还没做',
  activeLines: [{ label: 'Backlog (active)', text: 'y'.repeat(100), bytes: 100 }],
  ...over,
})

describe('findCompletedEntries', () => {
  it('S3a 命中标题被整条划掉', () => {
    const hits = findCompletedEntries('> 1. ~~已完成~~ —— ✅ 收口')
    expect(hits.map((h) => h.code)).toEqual(['S3a'])
  })

  it('S3b 命中标题段有 ✅ 但无划线', () => {
    const hits = findCompletedEntries('> 7. **✅ 已完成** 某事')
    expect(hits.map((h) => h.code)).toEqual(['S3b'])
  })

  it('放过 entry 内部子项的划线（仍 open 的条目）', () => {
    // ⚠️ 样本必须**忠实于真仓库的行长**：真实的条 5 长约 2 000 字符，内部划线与 ✅ 都远在
    //    120 字符标题窗口之外。计划里那份压缩样本只有 ~80 字符、把 ✅ 拉进了窗口内 ——
    //    照它的期望值改实现，等于给「`> N. **✅ 已完成** … ~~某子项~~」开一条逃逸口。
    const line =
      '> 5. **[[INFRA-F87]]** 主体已 ship，此处只留残余②：' +
      '那条闸只看具名 import，看不到编译器宏 / option 形态 / 同名 API 新签名。'.repeat(3) +
      '~~冒烟闸只覆盖默认导出面~~ → 已闭合 ✅'
    expect(findCompletedEntries(line)).toEqual([])
  })

  it('真仓库：内部含划线但标题未划的条目一条都不许被点名（非空过钉）', () => {
    const openList = sliceStatus(realStatus()).openList!
    const internalOnly = openList
      .split('\n')
      .filter((l) => /^> \d+\. /.test(l) && l.includes('~~') && !/^> \d+\. ~~/.test(l))
    // 非空过前置：这类行必须真的存在，否则本条断言什么也没验（设计期实测 4 条）
    expect(internalOnly.length).toBeGreaterThan(0)
    const flagged = findCompletedEntries(openList).map((h) => h.line)
    for (const l of internalOnly) expect(flagged).not.toContain(l)
  })

  it('✅ 出现在标题窗口之外不算 S3b', () => {
    // TITLE_WINDOW = 120 个**字符**（不是字节）—— repeat(40) = 160 字符，✅ 落在窗口外
    const line = '> 8. ' + '正常描述'.repeat(40) + ' ✅'
    expect(findCompletedEntries(line)).toEqual([])
  })

  it('✅ 落在标题窗口之内算 S3b（正向对照，防上一条空过）', () => {
    const line = '> 9. ' + '正常描述'.repeat(5) + ' ✅'
    expect(findCompletedEntries(line).map((h) => h.code)).toEqual(['S3b'])
  })
})

describe('evaluate', () => {
  it('顶部超阈值且无豁免 → S1 失败，且信息含搬迁目的地', () => {
    const { failures } = evaluate({
      slices: slicesOf({ top: 'z'.repeat(TOP_MAX + 1) }),
      exemptions: [],
    })
    expect(failures.map((f) => f.code)).toContain('S1')
    expect(failures.find((f) => f.code === 'S1').message).toContain('STATUS-CHANGELOG')
  })

  it('豁免在位且未上涨 → 放行', () => {
    const top = 'z'.repeat(TOP_MAX + 500)
    const ex = [
      {
        file: 'docs/STATUS.md',
        section: 'top-summary',
        code: 'S1',
        ceiling: TOP_MAX + 500,
        date: '2026-08-03',
        fix: '搬 CHANGELOG',
      },
    ]
    expect(evaluate({ slices: slicesOf({ top }), exemptions: ex }).failures).toEqual([])
  })

  it('豁免在位但涨了 1 B → 失败（上涨即红）', () => {
    const top = 'z'.repeat(TOP_MAX + 501)
    const ex = [
      {
        file: 'docs/STATUS.md',
        section: 'top-summary',
        code: 'S1',
        ceiling: TOP_MAX + 500,
        date: '2026-08-03',
        fix: '搬 CHANGELOG',
      },
    ]
    const { failures } = evaluate({ slices: slicesOf({ top }), exemptions: ex })
    expect(failures.map((f) => f.code)).toContain('S1')
  })

  it('已降到阈值以下但豁免还挂着 → 失败并要求删豁免（修完由闸自己宣布）', () => {
    const ex = [
      {
        file: 'docs/STATUS.md',
        section: 'top-summary',
        code: 'S1',
        ceiling: TOP_MAX + 500,
        date: '2026-08-03',
        fix: '搬 CHANGELOG',
      },
    ]
    const { failures } = evaluate({ slices: slicesOf(), exemptions: ex })
    expect(failures.find((f) => f.code === 'EXEMPT-STALE').message).toContain('删除')
  })

  it('切不出段 → fail-closed，不是放行', () => {
    const { failures } = evaluate({
      slices: { top: null, openList: null, activeLines: null },
      exemptions: [],
    })
    expect(failures.map((f) => f.code)).toContain('SLICE')
    expect(failures.length).toBeGreaterThan(0)
  })

  it('受管段为 0 字节 → fail-closed（整体删掉 ≠ 合规）', () => {
    const { failures } = evaluate({ slices: slicesOf({ top: '' }), exemptions: [] })
    expect(failures.map((f) => f.code)).toContain('SLICE')
  })

  it('Active 行超阈值 → S2 点名那一行', () => {
    const lines = [{ label: 'Backlog (active)', text: 'y', bytes: ACTIVE_LINE_MAX + 1 }]
    const { failures } = evaluate({ slices: slicesOf({ activeLines: lines }), exemptions: [] })
    expect(failures.find((f) => f.code === 'S2').message).toContain('Backlog (active)')
  })
})

describe('真豁免表 + 真仓库（INFRA-F95 待做② 清零后的终态）', () => {
  it('豁免表为空 —— 空是终态，不是待办', () => {
    // 2026-08-04 落地当天开的 3 条已全部由闸自己判 stale 后删除（顶部 10 346 → 1 630 B ·
    // Active 行 11 060 → 378 B · S3a 6 → 0 条）。再加行 = 先改这条测试，别悄悄放行。
    expect(EXEMPTIONS).toEqual([])
  })

  it('真 docs/STATUS.md 在真豁免表下零违例，且三段都非空（防空过）', () => {
    const slices = sliceStatus(realStatus())
    // 分母 fail closed：三段都得真切出来，否则「零违例」只是切空了
    expect(slices.top).not.toBeNull()
    expect(slices.openList).not.toBeNull()
    expect(slices.activeLines).not.toBeNull()
    expect(bytes(slices.top)).toBeGreaterThan(0)
    expect(slices.activeLines.length).toBeGreaterThan(0)
    expect(slices.activeLines.some((l) => l.label === 'Backlog (active)')).toBe(true)

    const { failures, report } = evaluate({ slices, exemptions: EXEMPTIONS })
    expect(failures).toEqual([])
    // 判据真的在受管段上量到了东西（不是 0 B 侥幸过关）
    expect(report.S1).toBeGreaterThan(0)
    expect(report.S1).toBeLessThanOrEqual(TOP_MAX)
    expect(report.S3a).toBe(0)
  })
})
