import {
  Permission,
  ROLE_PERMISSIONS,
  UserRole,
  ReleaseReason,
  RELEASE_REASON_META,
  ReleaseReasonSchema,
  releaseReasonsForForm,
  PlanEventReason,
  ABANDON_REASON_META,
  RECALL_FEEDBACK_OPTIONS,
} from '@pac/types';

/**
 * 召回分配 · 地基枚举回归(P0.1 + P0.2)。
 *
 * 这个文件锁的都是「改错了不会有任何编译错误」的地方 —— 全是运行时语义,
 * 类型系统救不了,只能靠断言。
 */

describe('PLAN_DISPATCH —— 主管判据', () => {
  test('⭐ 红线:staff 永远不能有 plan:dispatch', () => {
    // 加给 staff 就跟 PLAN_ASSIGN 一样废掉 —— 那条权限 staff 也有,
    // 正是"不能用它区分主管"的原因,新权限不能重蹈覆辙。
    expect(ROLE_PERMISSIONS[UserRole.STAFF]).not.toContain(Permission.PLAN_DISPATCH);
  });

  test('leader / admin 都有', () => {
    expect(ROLE_PERMISSIONS[UserRole.LEADER]).toContain(Permission.PLAN_DISPATCH);
    expect(ROLE_PERMISSIONS[UserRole.ADMIN]).toContain(Permission.PLAN_DISPATCH);
  });

  test('⭐ 现有权限里没有任何一条能替代它当主管判据', () => {
    // PLAN_ASSIGN:staff 也有(自助认领语义)→ 区分不了
    expect(ROLE_PERMISSIONS[UserRole.STAFF]).toContain(Permission.PLAN_ASSIGN);
    // PLAN_RECYCLE:同上
    expect(ROLE_PERMISSIONS[UserRole.STAFF]).toContain(Permission.PLAN_RECYCLE);
    // 于是 leader 相对 staff 只多这几条,PLAN_DISPATCH 是其中唯一为分配而立的
    const extra = ROLE_PERMISSIONS[UserRole.LEADER].filter(
      (p) => !ROLE_PERMISSIONS[UserRole.STAFF].includes(p),
    );
    expect(extra).toContain(Permission.PLAN_DISPATCH);
  });
});

describe('ReleaseReason —— 退回原因', () => {
  test('枚举与 zod schema 值域必须一致(加了枚举忘了改 schema → 服务端 400)', () => {
    expect([...ReleaseReasonSchema.options].sort()).toEqual([...Object.values(ReleaseReason)].sort());
  });

  test('每个值都在 META 里登记,且 META 无孤儿键', () => {
    expect(Object.keys(RELEASE_REASON_META).sort()).toEqual([...Object.values(ReleaseReason)].sort());
    for (const k of Object.values(ReleaseReason)) {
      expect(RELEASE_REASON_META[k].labelZh.length).toBeGreaterThan(0);
    }
  });

  test('⭐⭐ 红线:退回原因绝不能带抑制窗', () => {
    // 退回 = "换个人来做",不是"别做了"。照抄 ABANDON_REASON_META 的 suppressDays
    // 会把被退回的患者静默压 30~90 天 —— 池子里凭空少一批人,且没有任何报错。
    // TS 类型里已经不给这个字段,这里再从运行时锁一道(防有人改类型时顺手加回来)。
    for (const k of Object.values(ReleaseReason)) {
      expect(RELEASE_REASON_META[k]).not.toHaveProperty('suppressDays');
    }
    // 对照:放弃原因是有抑制窗的,两者语义不同不能混用
    expect(ABANDON_REASON_META.wrong_number.suppressDays).toBeGreaterThan(0);
  });

  test('⭐ 与召回反馈值域不重叠 —— 同名不同义是统计事故的标准配方', () => {
    // RECALL_FEEDBACK_OPTIONS.bad_timing = "召回时机不对(太早/太晚)",冲算法去的;
    // 退回的"时效太紧"冲派单去的。故本枚举刻意不设 bad_timing。
    const feedbackValues = RECALL_FEEDBACK_OPTIONS.map((o) => o.value as string);
    for (const k of Object.values(ReleaseReason)) {
      if (k === ReleaseReason.OTHER) continue; // other 是通用兜底键,允许同名
      expect(feedbackValues).not.toContain(k);
    }
  });

  test('other 必须填说明', () => {
    expect(RELEASE_REASON_META.other.needNote).toBe(true);
    // 其余的都不强制填,否则退回摩擦过大,客服会改走"关闭机会"污染放弃统计
    const needNote = Object.values(ReleaseReason).filter((k) => RELEASE_REASON_META[k].needNote);
    expect(needNote).toEqual([ReleaseReason.OTHER]);
  });

  test('每个原因都指向一根可调杠杆(否则原因分布只是好看的饼图)', () => {
    for (const k of Object.values(ReleaseReason)) {
      const { lever } = RELEASE_REASON_META[k];
      expect(lever).toBeTruthy();
      // 只有 other 允许无法归因
      if (k !== ReleaseReason.OTHER) expect(lever).not.toBe('none');
    }
  });

  test('表单清单过滤 hidden,且顺序 = 声明顺序', () => {
    const shown = releaseReasonsForForm();
    expect(shown).toEqual(Object.keys(RELEASE_REASON_META).filter(
      (k) => !RELEASE_REASON_META[k as ReleaseReason].hidden,
    ));
    expect(shown.length).toBe(8);
  });
});

describe('PlanEventReason —— plan_event_logs.reason 取值登记', () => {
  test('存量取值仍在(改名会让历史事件读不出来)', () => {
    // 这三个生产库里已有数据,枚举里的字面量**不能改**
    expect(PlanEventReason.UP).toBe('up');
    expect(PlanEventReason.DOWN).toBe('down');
    expect(PlanEventReason.TIMEOUT).toBe('timeout');
  });

  test('引擎补记账要用的两个新原因已登记', () => {
    expect(PlanEventReason.CLINIC_MOVED).toBe('clinic_moved');
    expect(PlanEventReason.SIGNALS_CLEARED).toBe('signals_cleared');
  });

  test('⭐ 系统原因与退回原因不得撞值 —— 撞了聚合就串了', () => {
    // 两者共用 reason 一列:auto_release 事件带系统原因,release 事件带退回原因。
    // 若有同值,「退回原因分布」的 SQL 即使按 event 过滤对了,人读也会混。
    const sys = Object.values(PlanEventReason) as string[];
    for (const r of Object.values(ReleaseReason)) {
      expect(sys).not.toContain(r as string);
    }
  });
});
