// pattern 索引查询入口的自测（纯函数，不读盘不打网络）。
//
// 真源 = scripts/pattern-index-lookup.mjs；规则真源 = AGENTS.md §Mockup 任务额外必读 第 11 项。
//
// **本文件钉的核心是一件事**：⛔ 查不到 ≠ 没有。
// 四态里只有 `covered` 能得出「这个产品已有 X」，另外三态的正确结论都是「我查不到」。
// 把它们合成一个 falsy 分支，就等于把「没测到」读成「0 个」—— 那正是整套健康度机制
// 反复警告的假趋势，只不过换到了人机交互面上。所以四态必须**可区分**，且各有各的修法。

import { describe, expect, it } from 'vitest'
// @ts-expect-error — plain ESM JS script without .d.ts
import { resolveFileKey, classify, coverage, orphanSummary } from '../scripts/pattern-index-lookup.mjs'

// ⚠️ 2026-08-14 起 target 上**没有** `patternIndex` opt-in 字段（白名单已拆）——
//    fixture 跟着去掉，别留一个真源里已不存在的字段（plan-fixture-is-not-the-criterion-source）。
const targets = [
  { fileKey: 'KEY_LCD', label: 'LCD', product: 'TVU Pack' },
  { fileKey: 'KEY_MH', label: 'MH-2026', product: 'Media Hub' },
]
const index = {
  files: {
    KEY_LCD: { label: 'LCD', product: 'TVU Pack', measuredAt: '2026-08-14', totals: { patterns: 27 }, patterns: [] },
  },
}

describe('fileKey 解析', () => {
  it('认 figma URL 的两种形态', () => {
    expect(resolveFileKey('https://www.figma.com/design/0054ib0nLmt27bC3QlGDl7/LCD?node-id=1-2', targets))
      .toEqual({ fileKey: '0054ib0nLmt27bC3QlGDl7', how: 'url' })
    expect(resolveFileKey('https://figma.com/file/ABCdef123456789xyz/Whatever', targets).how).toBe('url')
  })

  it('认裸 fileKey 与已登记的 label / product（大小写不敏感）', () => {
    expect(resolveFileKey('0054ib0nLmt27bC3QlGDl7', targets).how).toBe('key')
    expect(resolveFileKey('lcd', targets)).toEqual({ fileKey: 'KEY_LCD', how: 'label' })
    expect(resolveFileKey('Media Hub', targets)).toEqual({ fileKey: 'KEY_MH', how: 'label' })
  })

  it('⛔ 认不出就返回 null，**不猜** —— 猜错会查到另一个产品的 pattern，比查不到更坏', () => {
    expect(resolveFileKey('LC', targets).fileKey).toBeNull()       // label 的前缀，⛔ 不模糊匹配
    expect(resolveFileKey('状态卡', targets).fileKey).toBeNull()
    expect(resolveFileKey('', targets)).toEqual({ fileKey: null, how: 'empty' })
    expect(resolveFileKey(undefined, targets).fileKey).toBeNull()
  })
})

// 2026-08-14 首版实跑当场发现：拿指定库的 key 查，会落进 not-registered 并被引导去
// 「登记成测量目标」—— 而 targets JSON 的 $warning 逐字禁止这件事。⇒ 加了第五态。
describe('④ designated-library —— 查了设计库自己', () => {
  const DS = 'YbsPRUVmNdsbN40NNwh1Gn'

  it('判在最前：即使**已被误登记**也照样报设计库，不报 covered/registered', () => {
    expect(classify(DS, index, targets, DS).state).toBe('designated-library')
    const mis = [...targets, { fileKey: DS, label: '误登记', product: 'X', patternIndex: true }]
    expect(classify(DS, index, mis, DS).state).toBe('designated-library')
    const misIndexed = { files: { ...index.files, [DS]: { label: '误采', totals: { patterns: 1 } } } }
    expect(classify(DS, misIndexed, mis, DS).state).toBe('designated-library')
  })

  it('拿不到 designatedKey 时退化为四态，⛔ 不猜', () => {
    expect(classify(DS, index, targets, null).state).toBe('not-registered')
  })

  it('⛔ 不误伤产品文件', () => {
    expect(classify('KEY_LCD', index, targets, DS).state).toBe('covered')
    expect(classify('KEY_MH', index, targets, DS).state).toBe('not-collected')
  })
})

describe('⛔ 态必须可区分（查不到 ≠ 没有）', () => {
  it('① covered —— 唯一一个能得出「这个产品已有 X」的状态', () => {
    const r = classify('KEY_LCD', index, targets)
    expect(r.state).toBe('covered')
    expect(r.entry.totals.patterns).toBe(27)
  })

  it('② not-collected —— 在测量清单里但还没采过', () => {
    expect(classify('KEY_MH', index, targets)).toMatchObject({ state: 'not-collected' })
  })

  it('③ not-registered —— 不在测量清单里（⛔ 但照样能采）', () => {
    expect(classify('KEY_UNKNOWN', index, targets).state).toBe('not-registered')
  })

  it('⛔ covered 之外的态一律不是「没有」—— 只有 covered 能得出「已有 X」', () => {
    for (const k of ['KEY_MH', 'KEY_UNKNOWN']) expect(classify(k, index, targets).state).not.toBe('covered')
  })

  // 2026-08-14 opt-in 白名单被拆后，「已登记但没开采集」这个态**不再存在**：
  // 产物不存在 / 产物里没这个文件，对已登记目标而言都是同一件事（跑一次 --only），
  // 所以它们合并成 not-collected。⛔ 这不是把「没测到」读成「没有」—— 合并的两个态修法相同。
  it('⛔ 合并是因为修法相同，不是因为想少一个分支：产物缺失与条目缺失同态', () => {
    expect(classify('KEY_LCD', null, targets).state).toBe('not-collected')
    expect(classify('KEY_LCD', { files: {} }, targets).state).toBe('not-collected')
    // 而修法**不同**的两个（要不要顺便登记成长期测量目标）仍然分开
    expect(classify('KEY_MH', null, targets).state).not.toBe(classify('KEY_UNKNOWN', null, targets).state)
  })

  it('⛔ 白名单已拆：target 上有没有 patternIndex 字段都不再影响判定', () => {
    const legacy = [{ fileKey: 'KEY_MH', label: 'MH-2026', product: 'Media Hub', patternIndex: true }]
    expect(classify('KEY_MH', index, legacy).state).toBe('not-collected')
    expect(classify('KEY_MH', index, targets).state).toBe('not-collected')
  })
})

// DIM-U15（孤儿 master）的判据钉子。真源 = scripts/pattern-index-lookup.mjs 的 orphanSummary
// 头注释 + ds-health-dimensions.md §3.6。本组钉的核心也是同一件事的另一形态：
// **「没测到」⛔ 不等于「0 个」** —— 只不过这次「0」是个合法值，所以必须与 unknown 严格分开。
describe('孤儿 master 汇总（DIM-U15）', () => {
  const idx = {
    files: {
      A: { label: 'TPC', totals: { orphanMasters: 2, orphanInstances: 634, localInstances: 10210 } },
      B: { label: 'MicroApps', totals: { orphanMasters: 17, orphanInstances: 71, localInstances: 793 } },
      C: { label: 'MH', totals: { orphanMasters: 0, orphanInstances: 0, localInstances: 17 } },
    },
  }

  it('两个数并排给出，且按实例数降序（⛔ 不合成单一「孤儿率」）', () => {
    const s = orphanSummary(idx)
    expect(s.rows.map((r: any) => r.label)).toEqual(['TPC', 'MicroApps', 'MH'])
    // 同一份数据里两种形态并存：少 master 多实例 vs 多 master 少实例 —— 修法不同
    expect(s.rows[0]).toMatchObject({ masters: 2, instances: 634 })
    expect(s.rows[0].perMaster).toBeCloseTo(317, 5)
    expect(s.rows[1].perMaster).toBeCloseTo(71 / 17, 5)
    expect(s.rows[0].sharePct).toBeCloseTo((100 * 634) / 10210, 5)
  })

  // ⛔ 故障注入：把字段拿掉（= 早于本字段的旧快照，产物按 fileKey 合并所以真会出现）
  it('⛔ 字段缺失记 unknown、⛔ 不记 0，且不摊进合计分母', () => {
    const stale = { files: { ...idx.files, D: { label: '旧快照', totals: { localInstances: 500 } } } }
    const s = orphanSummary(stale)
    const d = s.rows.find((r: any) => r.label === '旧快照')
    expect(d).toMatchObject({ unknown: true, masters: null, instances: null, sharePct: null })
    expect(s.totals.unknownFiles).toBe(1)
    expect(s.totals.files).toBe(3)
    // 关键：那 500 个 file-local 实例**没有**被当成「0 个孤儿」摊进合计
    expect(s.totals.localInstances).toBe(10210 + 793 + 17)
    expect(s.totals.instances).toBe(634 + 71)
  })

  // 阴性对照：真的测到 0，必须是 0（不是 unknown）—— 否则上一条会退化成「一律记未知」的空过
  it('阴性对照：真测到 0 孤儿 ⇒ 记 0 并计入合计，⛔ 不记 unknown', () => {
    const s = orphanSummary({ files: { C: idx.files.C } })
    expect(s.rows[0]).toMatchObject({ unknown: false, masters: 0, instances: 0 })
    expect(s.totals).toMatchObject({ files: 1, unknownFiles: 0, masters: 0, instances: 0 })
    expect(s.totals.sharePct).toBe(0)
  })

  it('分母为 0 ⇒ 占比 null，⛔ 不给 0%（「没有分母」不是「没问题」）', () => {
    const s = orphanSummary({ files: { E: { label: '空文件', totals: { orphanMasters: 0, orphanInstances: 0, localInstances: 0 } } } })
    expect(s.rows[0].sharePct).toBeNull()
    expect(s.totals.sharePct).toBeNull()
  })

  it('产物不存在 / 空 ⇒ 空汇总，不崩', () => {
    expect(orphanSummary(null).rows).toEqual([])
    expect(orphanSummary({ files: {} }).totals).toMatchObject({ files: 0, unknownFiles: 0 })
  })
})

describe('覆盖面总览', () => {
  it('分出 covered / notCollected 两桶', () => {
    const c = coverage(index, targets)
    expect(c.covered.map((t: any) => t.label)).toEqual(['LCD'])
    expect(c.notCollected.map((t: any) => t.label)).toEqual(['MH-2026'])
    expect(c.adHocOrOrphan).toEqual([])
  })

  it('产物里有、targets 里没有 ⇒ ad-hoc 采过的（合法），单独列出不混进两桶', () => {
    const c = coverage({ files: { ...index.files, KEY_GHOST: {} } }, targets)
    expect(c.adHocOrOrphan).toEqual(['KEY_GHOST'])
    expect(c.covered.map((t: any) => t.label)).toEqual(['LCD'])
  })

  it('产物不存在时不崩，全部落进 notCollected', () => {
    const c = coverage(null, targets)
    expect(c.covered).toEqual([])
    expect(c.notCollected.map((t: any) => t.label)).toEqual(['LCD', 'MH-2026'])
  })
})
