import { buildSnoozeAnchors } from '../src/modules/plan/engine/plan-engine.service';

/**
 * 时间锚抑制(2026-07 修)单测 —— 纯函数层。
 * 背景:终态 plan 的"永久" snooze 原来按 (scenario|subKey) 一刀切,结案后**新发**的同类
 * 诊断(外院种植失败回院 / 多年后新发 @whole 病种)也被永久压死。修复 = 抑制集带结案锚点,
 * 引擎按 hit.latestSignalOccurredAt > anchor 放行。本文件测锚点构建;逃逸比较是一行大小关系,
 * 语义由锚点正确性保证。
 */
describe('buildSnoozeAnchors', () => {
  const d = (s: string) => new Date(s);
  const plan = (
    anchorExec: string | null,
    updatedAt: string,
    keys: Array<[string, string | null]>,
  ) => ({
    updatedAt: d(updatedAt),
    reasons: keys.map(([scenario, subKey]) => ({ scenario, subKey })),
    executions: anchorExec ? [{ createdAt: d(anchorExec) }] : [],
  });

  it('锚点取结案 execution.createdAt(不取 plan.updatedAt)', () => {
    // updatedAt 被召回反馈等后续写操作顶后(2026-07-10),真结案在 2026-07-01
    const anchors = buildSnoozeAnchors([
      plan('2026-07-01T10:00:00Z', '2026-07-10T00:00:00Z', [['recall', 'missing_tooth@36']]),
    ]);
    expect(anchors.get('recall|missing_tooth@36')).toEqual(d('2026-07-01T10:00:00Z'));
  });

  it('无 execution 时兜底 plan.updatedAt', () => {
    const anchors = buildSnoozeAnchors([
      plan(null, '2026-07-10T00:00:00Z', [['recall', 'perio@whole']]),
    ]);
    expect(anchors.get('recall|perio@whole')).toEqual(d('2026-07-10T00:00:00Z'));
  });

  it('同 key 多次结案取最新锚(最近一次处置才是新旧分界)', () => {
    const anchors = buildSnoozeAnchors([
      plan('2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z', [['recall', 'missing_tooth@36']]),
      plan('2026-06-01T00:00:00Z', '2026-06-01T00:00:00Z', [['recall', 'missing_tooth@36']]),
    ]);
    expect(anchors.get('recall|missing_tooth@36')).toEqual(d('2026-06-01T00:00:00Z'));
  });

  it('subKey=null 归一为空串 key(与引擎过滤口径一致)', () => {
    const anchors = buildSnoozeAnchors([
      plan('2026-07-01T00:00:00Z', '2026-07-01T00:00:00Z', [['recall', null]]),
    ]);
    expect(anchors.get('recall|')).toEqual(d('2026-07-01T00:00:00Z'));
  });

  it('多 plan 多 key 并集(多次结案累积)', () => {
    const anchors = buildSnoozeAnchors([
      plan('2026-05-01T00:00:00Z', '2026-05-01T00:00:00Z', [['recall', 'caries@45']]),
      plan('2026-06-01T00:00:00Z', '2026-06-01T00:00:00Z', [['recall', 'perio@whole']]),
    ]);
    expect(anchors.size).toBe(2);
    expect(anchors.get('recall|caries@45')).toEqual(d('2026-05-01T00:00:00Z'));
    expect(anchors.get('recall|perio@whole')).toEqual(d('2026-06-01T00:00:00Z'));
  });

  // 逃逸语义端到端(用与引擎 336 行同款比较式,防口径漂移)
  it('逃逸判定:结案后新发信号(latest > anchor)放行;老信号照压', () => {
    const anchors = buildSnoozeAnchors([
      plan('2026-06-01T00:00:00Z', '2026-06-01T00:00:00Z', [['recall', 'missing_tooth@36']]),
    ]);
    const escapes = (key: string, latest: Date | null) => {
      const anchor = anchors.get(key);
      if (!anchor) return true;
      return latest != null && latest > anchor;
    };
    // 老 fact(结案前 2024)聚类 → latest=2024 → 仍压
    expect(escapes('recall|missing_tooth@36', d('2024-03-01T00:00:00Z'))).toBe(false);
    // 结案后新发(2027 复诊再诊断)→ latest=2027 → 放行复活
    expect(escapes('recall|missing_tooth@36', d('2027-01-15T00:00:00Z'))).toBe(true);
    // 旧 scenario 未提供 latest → 维持旧行为全压
    expect(escapes('recall|missing_tooth@36', null)).toBe(false);
    // 不在抑制集 → 放行
    expect(escapes('recall|caries@45', d('2024-01-01T00:00:00Z'))).toBe(true);
  });
});
