import { EXECUTION_OUTCOME_META, EXECUTION_OUTCOME_GROUP_META } from '@pac/types';

/**
 * 执行结果的**归类**与**状态机**是两件事,这个 spec 锁的就是它们的分离。
 *
 * 🔴 有两项 `group` 与 `drivesStatus` **故意不一致**,看着像笔误,极易被后来的人"顺手改一致":
 *
 *   `scheduled_next`(约定下次回访)
 *     group:        'close'  —— 统计口径:约到下次是**有效推进**,不该跟"未接通"一起算「保持」
 *     drivesStatus: 'keep'   —— 状态机:工单必须留着并 snooze 到回访日
 *   改成 'completed' 的后果:客服刚答应患者"那天再联系您",这单却当场出池,
 *   **约好的回访再也不会发生**,且不报错。
 *
 *   `quick_hangup`(秒挂,2026-08-06 产品定)
 *     group:        'give_up' —— 统计口径:**接通了、被主动挂断**,是一次失败的触达
 *     drivesStatus: 'keep'    —— 状态机:可能只是在开车/开会,⛔ 不能当永久拒绝结掉单
 *   改成 'abandoned' 的后果:一个只是时机不对的患者被永久关掉。
 */
describe('执行结果分类 —— 归类口径 ≠ 状态机', () => {
  test('⭐⭐ 约定下次回访:算「成功」但**不结案**', () => {
    const m = EXECUTION_OUTCOME_META.scheduled_next;
    expect(m.group).toBe('close'); // 统计上算成功
    expect(m.drivesStatus).toBe('keep'); // ⛔ 绝不能是 completed
  });

  test('⭐ 转化新预约:算「成功」且结案(顶栏「预约」快速记录落的就是它)', () => {
    const m = EXECUTION_OUTCOME_META.success_appointed;
    expect(m.group).toBe('close');
    expect(m.drivesStatus).toBe('completed');
  });

  test('⭐⭐ 秒挂:算「不成功」但**不结案**,且必须有冷静期', () => {
    const m = EXECUTION_OUTCOME_META.quick_hangup;
    // 接通了却被挂断 = 联系上了但被拒绝,⛔ 别跟"没联系上"混为一谈
    expect(m.group).toBe('give_up');
    expect(m.drivesStatus).toBe('keep'); // ⛔ 绝不能是 abandoned
    // ⛔ 不能像未接通那样 null —— 人家刚挂断你,当天回拨是骚扰
    expect(m.suppressDays).toBeGreaterThan(0);
    // ⚠️ 比「再考虑」(7 天)**更长**:再考虑是患者答应了会想想,该按时回来问;
    //    秒挂是连话都不想说,再快回去只会更烦。⛔ 别"顺手"改成比它短。
    expect(m.suppressDays!).toBeGreaterThan(EXECUTION_OUTCOME_META.declined_recent.suppressDays!);
    // 但仍轻于「明确拒绝」—— 秒挂没说过任何拒绝的话
    expect(m.suppressDays!).toBeLessThan(EXECUTION_OUTCOME_META.refused.suppressDays!);
  });

  test('⭐ 同一组里颜色必须一致 —— 选中时的边框色取自 tone', () => {
    // 实测走查:秒挂改判「不成功」后 tone 还留着灰(未接通那档),
    // 主管在同一组里看到一个异色项,以为自己点错了。
    const tones = new Set(
      Object.entries(EXECUTION_OUTCOME_META)
        .filter(([, m]) => m.group === 'give_up' && !m.hiddenInForm)
        .map(([, m]) => m.tone),
    );
    expect([...tones]).toEqual(['rose']);
  });

  test('「保持」组里只剩**真正没联系上**的(未接通)', () => {
    const keep = Object.entries(EXECUTION_OUTCOME_META)
      .filter(([, m]) => m.group === 'keep' && !m.hiddenInForm)
      .map(([k]) => k)
      .sort();
    // ⛔ 秒挂不在这里:它是"联系上了但被拒绝",与"打不通"要改的东西完全相反
    //    (换时段 vs 换话术/换人群),混在一起两个信号互相稀释。
    expect(keep).toEqual(['no_answer']);
  });

  test('未接通**不设**冷静期 —— 没联系上就该尽快再试', () => {
    expect(EXECUTION_OUTCOME_META.no_answer.suppressDays).toBeNull();
  });

  test('⛔ 归到「成功」组的,要么结案、要么留单 —— 但**绝不能**是 abandoned', () => {
    // abandoned 是"放弃",出现在成功组等于把放弃算成成功,统计当场反向
    for (const [key, m] of Object.entries(EXECUTION_OUTCOME_META)) {
      if (m.group === 'close') {
        expect([key, m.drivesStatus]).not.toEqual([key, 'abandoned']);
      }
    }
  });

  test('每个 outcome 的 group 都必须是已登记的组(拼错会静默从面板消失)', () => {
    const groups = new Set(Object.keys(EXECUTION_OUTCOME_GROUP_META));
    for (const [key, m] of Object.entries(EXECUTION_OUTCOME_META)) {
      expect([key, groups.has(m.group)]).toEqual([key, true]);
    }
  });
});
