import {
  EXECUTION_OUTCOME_META,
  SUPPRESS_PERMANENT_DAYS,
  BREAKER_SUPPRESS_DAYS,
} from '@pac/types';
import { resolveSnoozedUntil } from '../src/modules/plan/recall-suppression';
import { computeLikelihoodBonus } from '../src/modules/plan/engine/priority-scorer';

/**
 * 召回闭环:熔断 / 拒绝 / 回访冷静期(snoozedUntil)。
 *
 * 修复的核心 bug(GAP1):终态 plan(completed/abandoned)在重算时被当成"没有活动 plan"
 *   直接新建一版 active → "明确拒绝 / 4 次未接通熔断"第二天就复活、患者重回呼叫池。
 * 现在 execution 回写按 outcome 算 snoozedUntil,plan 引擎 upsert 在抑制期内返回 suppressed。
 */
describe('召回抑制窗 snoozedUntil 计算', () => {
  const NOW = new Date('2026-06-01T00:00:00Z');
  const daysFromNow = (d: Date | null) =>
    d == null ? null : Math.round((d.getTime() - NOW.getTime()) / 86_400_000);

  test('明确拒绝 refused → 90d 冷静期', () => {
    const r = resolveSnoozedUntil({ outcome: 'refused', breakerTripped: false, now: NOW });
    expect(daysFromNow(r)).toBe(90);
  });

  test('近期不考虑 declined_recent → 90d(虽 keep 类,仍冷静期内不进池)', () => {
    const r = resolveSnoozedUntil({ outcome: 'declined_recent', breakerTripped: false, now: NOW });
    expect(daysFromNow(r)).toBe(90);
  });

  test('成功转化 success_appointed → 60d(覆盖预约→治疗→摄入延迟,避免重复召)', () => {
    const r = resolveSnoozedUntil({ outcome: 'success_appointed', breakerTripped: false, now: NOW });
    expect(daysFromNow(r)).toBe(60);
  });

  test('客服放弃 abandoned → 60d', () => {
    const r = resolveSnoozedUntil({ outcome: 'abandoned', breakerTripped: false, now: NOW });
    expect(daysFromNow(r)).toBe(60);
  });

  test('外院治疗 external_treatment → 永久(真闭环)', () => {
    const r = resolveSnoozedUntil({ outcome: 'external_treatment', breakerTripped: false, now: NOW });
    expect(daysFromNow(r)).toBe(SUPPRESS_PERMANENT_DAYS);
  });

  test('标记无效 marked_invalid → 永久', () => {
    const r = resolveSnoozedUntil({ outcome: 'marked_invalid', breakerTripped: false, now: NOW });
    expect(daysFromNow(r)).toBe(SUPPRESS_PERMANENT_DAYS);
  });

  test('熔断优先于 outcome 自身策略 → 固定 30d(联系不上,非拒绝,换天再试)', () => {
    // outcome=no_answer 本身 suppressDays=null,但熔断触发 → 30d
    const r = resolveSnoozedUntil({ outcome: 'no_answer', breakerTripped: true, now: NOW });
    expect(daysFromNow(r)).toBe(BREAKER_SUPPRESS_DAYS);
  });

  test('熔断即便发生在终态 outcome 上,也用 30d 而非 outcome 的窗', () => {
    // 防御:breaker 优先级最高
    const r = resolveSnoozedUntil({ outcome: 'refused', breakerTripped: true, now: NOW });
    expect(daysFromNow(r)).toBe(BREAKER_SUPPRESS_DAYS);
  });

  test('keep 类带 scheduledNextAt(约定回访)→ snooze 到该时间点', () => {
    const next = '2026-06-15T02:00:00Z';
    const r = resolveSnoozedUntil({
      outcome: 'scheduled_next',
      breakerTripped: false,
      scheduledNextAt: next,
      now: NOW,
    });
    expect(r?.toISOString()).toBe(new Date(next).toISOString());
  });

  test('考虑中 considering 无 scheduledNextAt → 不 snooze(null,维持现状)', () => {
    const r = resolveSnoozedUntil({ outcome: 'considering', breakerTripped: false, now: NOW });
    expect(r).toBeNull();
  });

  test('未接通 no_answer 无 scheduledNextAt → 不 snooze', () => {
    const r = resolveSnoozedUntil({ outcome: 'no_answer', breakerTripped: false, now: NOW });
    expect(r).toBeNull();
  });
});

describe('优先级:considering 不再加权(GAP2)', () => {
  test('考虑中不应把患者顶到列表最前(冷静期内 + 不加权)', () => {
    expect(computeLikelihoodBonus(3, [{ outcome: 'considering' }])).toBe(0);
  });

  test('真实正向进展(成功转化 / 改约)仍 +4', () => {
    expect(computeLikelihoodBonus(3, [{ outcome: 'success_appointed' }])).toBe(4);
    expect(computeLikelihoodBonus(3, [{ outcome: 'rescheduled' }])).toBe(4);
  });

  test('无近期正向执行 → 仅 riskBonus', () => {
    expect(computeLikelihoodBonus(3, [{ outcome: 'no_answer' }])).toBe(0); // risk=3 → riskBonus 0
    expect(computeLikelihoodBonus(0, [])).toBe(6); // risk=0 → (3-0)*2=6
  });
});

describe('抑制策略单一真理源 EXECUTION_OUTCOME_META.suppressDays', () => {
  test('每个 outcome 都声明了 suppressDays(number 或 null),无遗漏', () => {
    for (const [k, v] of Object.entries(EXECUTION_OUTCOME_META)) {
      expect(v).toHaveProperty('suppressDays');
      expect(v.suppressDays === null || typeof v.suppressDays === 'number').toBe(true);
      if (typeof v.suppressDays === 'number') {
        expect(v.suppressDays).toBeGreaterThan(0);
      }
      void k;
    }
  });
});
