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;
}

/** @param count 本批给他几条 @param inHandBefore 分配前他手上还有几条 */
const agent = (name: string, count: number, inHandBefore = 0) =>
  ({
    userId: name,
    name,
    inHandBefore,
    count,
    dedicated: 0,
    spread: count,
    loadAfter: inHandBefore + count,
    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.map((o) => o.intent)).toEqual([
      ASSIGNMENT_INTENTS.EXPIRY_SET,
      ASSIGNMENT_INTENTS.DAILY_RATE_SET,
      ASSIGNMENT_INTENTS.BATCH_SIZE_SET,
    ]);
    expect(s.options.some((o) => o.args?.days === 4)).toBe(true);
  });

  /**
   * 🔴 这个节点**每批基本都会亮**（N 只算新增、不扣在手），产品定这是对的 ——
   * 它是工作量预估，不是异常。⇒ 措辞不许像报错，否则常亮就变成"系统天天出故障"。
   */
  test('🔴 措辞不许像报错，且必须把「本批 / 原本在手」拆开说', () => {
    // 本批 45 + 原本在手 15 = 60 —— 拆开才看得出这批只压了 45
    const p = proposal({ expiresInDays: 3, byAgent: [agent('张悦', 45, 15)] });
    const s = computeSignals(p).find((x) => x.key === 'daily_overload')!;
    for (const w of ['打不完', '超了', '过载', '超出', '警告', '异常']) {
      expect(`${s.title}${s.why}`).not.toContain(w);
    }
    // ⚠️ 增量与总量必须分开 —— 不拆开主管会以为这批一下压了 60 条给他
    expect(s.why).toContain('本批 45 条');
    expect(s.why).toContain('原本在手 15 条');
    // ⚠️ 默认路径要说清后果是可接受的（落回池子、下批还能分），⛔ 不制造紧迫感
    expect(s.defaultLabel).toContain('落回池子');
  });

  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\./);
    }
  });
});
