import { AnchorMode, type AssignmentProposal } from '@pac/types';
import { ASSIGNMENT_INTENTS, computeSignals } from '../src/modules/plan/assignment-signals';

/**
 * 引导节点的判定 —— 规格见 docs/design/assignment-agent-flow.md §五。
 *
 * 这里锁的是**判定本身**（什么情况该出、severity 怎么排、默认是不是 no-op），
 * ⛔ 不锁措辞 —— 措辞会随产品走查改，锁住它只会让每次改文案都红一片。
 */

/** 最小提案骨架:只填判定要用到的字段,其余给零值。 */
function proposal(over: Partial<AssignmentProposal> = {}): AssignmentProposal {
  return {
    clinicId: 'c1',
    potentialTreatment: 'implant',
    temperature: 'hot',
    anchorMode: AnchorMode.DIAGNOSIS,
    candidateTotal: 1000,
    target: 405,
    batchSize: 405,
    expiresInDays: 3,
    basis: 'explicit',
    basisFrom: null,
    agentOverrides: {},
    placed: 405,
    unplaced: 0,
    items: [],
    pending: [],
    canRefill: false,
    poolOwnership: null,
    byAgent: [],
    skippedAgents: [],
    rosterNote: '',
    basisNote: '',
    selectionNote: '',
    pendingNote: '',
    refillNote: '',
    opsNote: '',
    signals: [],
    ...over,
  } as AssignmentProposal;
}

const agent = (name: string, loadAfter: number) =>
  ({
    userId: name,
    name,
    inHandBefore: 0,
    count: loadAfter,
    dedicated: 0,
    spread: loadAfter,
    loadAfter,
    expiresInDays: 3,
    overridden: false,
  }) as AssignmentProposal['byAgent'][number];

const keys = (p: AssignmentProposal) => computeSignals(p).map((s) => s.key);

describe('引导节点 · 判定', () => {
  test('⭐ 一切正常 → 一个节点都不出', () => {
    expect(keys(proposal())).toEqual([]);
  });

  test('🔴 有待分配 → 出 pending，且 severity 最高（有人会被静默漏掉）', () => {
    const p = proposal({
      pending: [{}] as AssignmentProposal['pending'],
      byAgent: [agent('张悦', 10)],
      basis: 'default',
    });
    const s = computeSignals(p);
    expect(s[0]!.key).toBe('pending');
    expect(s[0]!.tier).toBe('action');
    // ⚠️ 排在纯信息类之前
    expect(s.map((x) => x.severity)).toEqual([...s.map((x) => x.severity)].sort((a, b) => a - b));
  });

  test('⭐ canRefill=false 时，「换无主患者补上」这个选项不出现（⛔ 不给做不到的选项）', () => {
    const no = computeSignals(proposal({ pending: [{}] as AssignmentProposal['pending'] }))[0]!;
    expect(no.options.map((o) => o.intent)).not.toContain(ASSIGNMENT_INTENTS.PENDING_REFILL);
    const yes = computeSignals(
      proposal({ pending: [{}] as AssignmentProposal['pending'], canRefill: true }),
    )[0]!;
    expect(yes.options.map((o) => o.intent)).toContain(ASSIGNMENT_INTENTS.PENDING_REFILL);
  });

  test('🔴 分完之后最重的那位在时效内打不完 → 出 daily_overload，并给出要几天', () => {
    // 每天 15 通 × 3 天 = 45 条是刚好打得完的量；60 条要 4 天
    const p = proposal({ expiresInDays: 3, byAgent: [agent('李莉', 30), agent('张悦', 60)] });
    const s = computeSignals(p).find((x) => x.key === 'daily_overload')!;
    expect(s).toBeDefined();
    expect(s.title).toContain('张悦');
    expect(s.why).toContain('4 天');
    expect(s.options.some((o) => o.args?.days === 4)).toBe(true);
  });

  test('⭐ 刚好打得完（45 条 / 3 天）→ ⛔ 不出 daily_overload', () => {
    expect(keys(proposal({ expiresInDays: 3, byAgent: [agent('张悦', 45)] }))).not.toContain(
      'daily_overload',
    );
  });

  test('⭐ 候选不够 → 出 short_supply', () => {
    expect(keys(proposal({ candidateTotal: 44, target: 44, batchSize: 405 }))).toContain(
      'short_supply',
    );
  });

  test('⭐ 用的是系统默认基数 → 出 expiry_default，且 tier=info（可折叠）', () => {
    const s = computeSignals(proposal({ basis: 'default', byAgent: [agent('a', 1)] })).find(
      (x) => x.key === 'expiry_default',
    )!;
    expect(s.tier).toBe('info');
  });

  test('⭐ 按末诊口径 → 出 anchor_nondefault；按诊断（默认）→ 不出', () => {
    expect(keys(proposal({ anchorMode: AnchorMode.LAST_VISIT }))).toContain('anchor_nondefault');
    expect(keys(proposal({ anchorMode: AnchorMode.DIAGNOSIS }))).not.toContain('anchor_nondefault');
  });

  test('🔴 每个节点的默认都是 no-op —— 「一条不点直接确认」必须永远安全', () => {
    const p = proposal({
      pending: [{}] as AssignmentProposal['pending'],
      candidateTotal: 44,
      target: 44,
      basis: 'default',
      anchorMode: AnchorMode.LAST_VISIT,
      expiresInDays: 3,
      byAgent: [agent('张悦', 90)],
    });
    const s = computeSignals(p);
    expect(s.length).toBeGreaterThanOrEqual(5);
    // ⚠️ 判据:每个 defaultLabel 都在描述"不处理会怎样",⛔ 没有一个是要主管动手的
    for (const x of s) expect(x.defaultLabel).toMatch(/^不处理 = |^不/);
  });

  test('🔴 ⛔ 不截断 —— 五个条件同时成立就出五个，防噪音靠 tier 分层', () => {
    const p = proposal({
      pending: [{}] as AssignmentProposal['pending'],
      candidateTotal: 44,
      target: 44,
      basis: 'default',
      anchorMode: AnchorMode.LAST_VISIT,
      byAgent: [agent('张悦', 90)],
    });
    const s = computeSignals(p);
    expect(s.length).toBe(5);
    expect(s.filter((x) => x.tier === 'action').map((x) => x.key)).toEqual([
      'pending',
      'daily_overload',
      'short_supply',
    ]);
    expect(s.filter((x) => x.tier === 'info').map((x) => x.key)).toEqual([
      'expiry_default',
      'anchor_nondefault',
    ]);
  });

  test('⛔ 模型拿不到 options 的技术细节 —— why 里不许出现 intent id', () => {
    const s = computeSignals(
      proposal({ pending: [{}] as AssignmentProposal['pending'], canRefill: true }),
    );
    for (const x of s) {
      expect(x.why).not.toMatch(/pending\.|expiry\.|cohort\./);
      expect(x.title).not.toMatch(/pending\.|expiry\.|cohort\./);
    }
  });
});
