// ds-health-delta 的 pattern 索引读法自测（纯函数，不读盘不跑 git）。
//
// 真源 = scripts/ds-health-delta.mjs；维度真源 = docs/internal/ds-health-dimensions.md §3.6（DIM-U15）。
//
// **本文件钉的核心**：时间序列里**有**数据 ≠ 有人**读**得出趋势。
// 采集器从一开始就把 `patternIndex` 写进 ds-health-history.jsonl 的每个 result 行，
// 但 delta 侧只读 wrongLibraryRate 那一族 ⇒ DIM-U15 只有当前态快照、**没有趋势**。
// 本组测试钉的是那个读法，以及它必须守住的三条纪律：
//   ① 上一点**没有该字段** ⇒ unknown，⛔ 不当 0（真实会发生：08-13 那行零个、08-14 只有 LCD）
//   ② 阴性对照：真测到 0 ⇒ 必须记 0 —— 否则 ① 会退化成「一律记未知」的空过
//   ③ ⛔ 不合成「孤儿率」：master 数与实例数**各自独立**给出，形态相反时两边都要如实报

import { describe, expect, it } from 'vitest'
// @ts-expect-error — plain ESM JS script without .d.ts
import { patternIndexDelta } from '../scripts/ds-health-delta.mjs'

const pi = (o: Record<string, number>) => ({
  patterns: 0, localInstances: 0, orphanMasters: 0, orphanInstances: 0,
  unexplained: 0, genericallyNamed: 0, zeroInstance: 0, ...o,
})

const prev = {
  kind: 'mockup-library-origin',
  measuredAt: '2026-08-14',
  results: [
    // 白名单时代唯一被采到 pattern 索引的那个文件
    { fileKey: 'K_LCD', label: 'LCD', status: 'ok', patternIndex: pi({ patterns: 27, localInstances: 1604, orphanMasters: 4, orphanInstances: 14 }) },
    // 同轮测了库归属、但**没有** patternIndex 字段 —— 本轮才第一次有
    { fileKey: 'K_MH', label: 'MH-2026', status: 'ok' },
    // 上一点取证失败
    { fileKey: 'K_ERR', label: 'Config-T', status: 'error', error: 'GET /files → 403 Forbidden' },
    // 上一点**真测到 0 个孤儿**（阴性对照的来源）
    { fileKey: 'K_ZERO', label: 'PP-2025-2026', status: 'ok', patternIndex: pi({ patterns: 5, localInstances: 17 }) },
  ],
}

const cur = {
  kind: 'mockup-library-origin',
  measuredAt: '2026-08-14',
  results: [
    // master ▼ 但实例 ▲ —— 两种形态并存，⛔ 不许被合成成一个方向
    { fileKey: 'K_LCD', label: 'LCD', status: 'ok', patternIndex: pi({ patterns: 30, localInstances: 1610, orphanMasters: 2, orphanInstances: 600 }) },
    { fileKey: 'K_MH', label: 'MH-2026', status: 'ok', patternIndex: pi({ patterns: 12, localInstances: 300, orphanMasters: 1, orphanInstances: 3 }) },
    { fileKey: 'K_ERR', label: 'Config-T', status: 'ok', patternIndex: pi({ patterns: 9, localInstances: 88, orphanMasters: 0, orphanInstances: 0 }) },
    { fileKey: 'K_ZERO', label: 'PP-2025-2026', status: 'ok', patternIndex: pi({ patterns: 6, localInstances: 20, orphanMasters: 3, orphanInstances: 9 }) },
    // 本轮新出现的目标（上一点整条不在）
    { fileKey: 'K_NEW', label: 'Media Service UR', status: 'ok', patternIndex: pi({ patterns: 3, localInstances: 40 }) },
  ],
}

const rowOf = (rows: any[], fileKey: string) => rows.find((r) => r.fileKey === fileKey)

describe('DIM-U15 趋势读法（patternIndexDelta）', () => {
  it('两点都有字段 ⇒ 给出 delta，且 master 数与实例数**各自独立**（⛔ 不合成孤儿率）', () => {
    const r = rowOf(patternIndexDelta(prev, cur), 'K_LCD')
    expect(r.state).toBe('ok')
    // 形态相反：master ▼2 而实例 ▲586 —— 两边都要如实报，合成一个数就把这件事抹平了
    expect(r.delta.orphanMasters).toBe(-2)
    expect(r.delta.orphanInstances).toBe(586)
    expect(r.delta.patterns).toBe(3)
    expect(r.delta.localInstances).toBe(6)
    // ⛔ 结构性钉子：不许出现任何单一「孤儿率」字段（§3.6 逐字：合成会把两种修法搅成一团）
    expect(Object.keys(r.delta)).not.toContain('orphanRate')
  })

  it('⛔ 故障注入：上一点**没有 patternIndex 字段** ⇒ unknown，⛔ 不把缺失当 0', () => {
    const r = rowOf(patternIndexDelta(prev, cur), 'K_MH')
    expect(r.state).toBe('no-baseline')
    expect(r.delta).toBeNull()
    // 关键：本轮的 1 个 master / 3 个实例 ⛔ 不许被渲染成「从 0 涨到 1」
    expect(r.prev).toBeNull()
    expect(r.cur.orphanMasters).toBe(1)
  })

  it('阴性对照：上一点**真测到 0** 孤儿 ⇒ 照常算 delta，⛔ 不记 unknown', () => {
    const r = rowOf(patternIndexDelta(prev, cur), 'K_ZERO')
    expect(r.state).toBe('ok')
    expect(r.delta.orphanMasters).toBe(3)
    expect(r.delta.orphanInstances).toBe(9)
  })

  it('上一点取证失败 ⇒ 无同口径对照，⛔ 不打 delta 也不记 0', () => {
    const r = rowOf(patternIndexDelta(prev, cur), 'K_ERR')
    expect(r.state).toBe('baseline-error')
    expect(r.delta).toBeNull()
  })

  it('本轮取证失败 ⇒ 不打 delta（与采集器「没测到 ≠ 0 个问题」同纪律）', () => {
    const broken = { ...cur, results: [{ fileKey: 'K_LCD', label: 'LCD', status: 'error', error: 'timeout' }] }
    const r = rowOf(patternIndexDelta(prev, broken), 'K_LCD')
    expect(r.state).toBe('current-error')
    expect(r.delta).toBeNull()
  })

  it('本轮 ok 但**缺 patternIndex 字段** ⇒ unknown（旧采集器 / 未来字段被删都会长这样）', () => {
    const bare = { ...cur, results: [{ fileKey: 'K_LCD', label: 'LCD', status: 'ok' }] }
    const r = rowOf(patternIndexDelta(prev, bare), 'K_LCD')
    expect(r.state).toBe('no-current-field')
    expect(r.delta).toBeNull()
  })

  it('本轮新出现的文件 ⇒ new-file，无对照点', () => {
    const r = rowOf(patternIndexDelta(prev, cur), 'K_NEW')
    expect(r.state).toBe('new-file')
    expect(r.delta).toBeNull()
    expect(r.cur.patterns).toBe(3)
  })

  it('attributed 由对账恒等式派生（local − 孤儿 − 未解释），⛔ 不另存一份会漂的字段', () => {
    const r = rowOf(patternIndexDelta(prev, cur), 'K_LCD')
    expect(r.prev.attributed).toBe(1604 - 14 - 0)
    expect(r.cur.attributed).toBe(1610 - 600 - 0)
    expect(r.delta.attributed).toBe(1010 - 1590)
  })

  it('unexplained ≠ 0 ⇒ 标出对账破了（判据有洞，⛔ 别当噪声）', () => {
    const holed = { ...cur, results: [{ ...cur.results[0], patternIndex: pi({ patterns: 30, localInstances: 1610, orphanMasters: 2, orphanInstances: 600, unexplained: 7 }) }] }
    const r = rowOf(patternIndexDelta(prev, holed), 'K_LCD')
    expect(r.reconciliationBroken).toBe(true)
    // 阴性对照：闭合时不许误报
    expect(rowOf(patternIndexDelta(prev, cur), 'K_LCD').reconciliationBroken).toBe(false)
  })

  it('空 / 畸形输入不崩（第一次跑、只有一个点时会真的传进来）', () => {
    expect(patternIndexDelta(null, null)).toEqual([])
    expect(patternIndexDelta(prev, { results: [] })).toEqual([])
    expect(patternIndexDelta({}, cur).every((r: any) => r.state === 'new-file')).toBe(true)
  })
})
