import {
  EXECUTION_OUTCOME_META,
  SUPPRESS_PERMANENT_DAYS,
  BREAKER_SUPPRESS_DAYS,
  CLOSE_REASON_META,
  CLOSE_REASON_DEFAULT_SUPPRESS_DAYS,
} from '@pac/types';
import { resolveSnoozedUntil } from '../src/modules/plan/recall-suppression';
import {
  computeConfidenceFactor,
  computeFreshness,
  calcPriority,
  type PriorityInput,
} 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 → 7d(keep 非终态,7天冷静期后自动浮现)', () => {
    const r = resolveSnoozedUntil({ outcome: 'declined_recent', breakerTripped: false, now: NOW });
    expect(daysFromNow(r)).toBe(7);
  });

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

/**
 * 优先级 v3.0 三维模型(7eb8dbe 重构后)。
 * 原 GAP2"considering 不加权":v3 scorer 不再消费 execution outcome(likelihood bonus 已删),
 * 由设计保证 —— 这里改锁 v3 的确定性因子(置信度 / 新鲜度 / 三维加权)。
 */
describe('priority-scorer v3 | 确定性因子', () => {
  test('置信度因子:直用来源置信度,夹在 [0.5, 1](防过罚)', () => {
    expect(computeConfidenceFactor(1)).toBe(1); // 医生诊断
    expect(computeConfidenceFactor(0.8)).toBe(0.8); // 建议
    expect(computeConfidenceFactor(0.2)).toBe(0.5); // 下限地板
    expect(computeConfidenceFactor(1.5)).toBe(1); // 上限
  });

  test('新鲜度:黄金窗内满权重,过窗线性衰减,2×窗见地板 0.4', () => {
    expect(computeFreshness(0, 180)).toBe(1.0);
    expect(computeFreshness(180, 180)).toBe(1.0); // 窗上界仍满
    expect(computeFreshness(270, 180)).toBeCloseTo(0.7); // 过窗一半
    expect(computeFreshness(360, 180)).toBeCloseTo(0.4); // 2×窗
    expect(computeFreshness(3600, 180)).toBeCloseTo(0.4); // 地板不再降
  });

  const baseInput: PriorityInput = {
    urgencyLevel: 'high',
    daysSince: 0,
    windowDays: 180,
    primaryCode: 'K02',
    toothCount: 1,
    rfmSegment: null,
    consultIntentMatch: false,
    lifecycleStage: null,
    hasTreatmentHistory: false,
    isReferralChampion: false,
    recentVisitGood: false,
    confidence: 1,
  };

  test('三维加权:急迫×0.4 + 价值×0.3 + 意愿×0.3(缺省 rfm=5/意向=2/信任=4)', () => {
    const r = calcPriority(baseInput);
    // urgency high=7 → 2.8;K02 价值 2 → 0.6;意愿 5*0.375+2*0.375+4*0.25=3.625 → 1.0875
    expect(r.breakdown.urgency).toBe(7);
    expect(r.breakdown.value).toBe(2);
    expect(r.breakdown.willingness).toBe(3.63);
    expect(r.breakdown.base).toBe(4.49);
    expect(r.score).toBe(45); // round(4.4875 × 10)
  });

  test('种植(K08)价值按牙数分档:单颗8 / 多颗9 / 半口+10', () => {
    expect(calcPriority({ ...baseInput, primaryCode: 'K08', toothCount: 1 }).breakdown.value).toBe(8);
    expect(calcPriority({ ...baseInput, primaryCode: 'K08', toothCount: 3 }).breakdown.value).toBe(9);
    expect(calcPriority({ ...baseInput, primaryCode: 'K08', toothCount: 6 }).breakdown.value).toBe(10);
  });

  test('修复(K03)≥3颗提档:5 → 6;未映射病种价值兜底 3', () => {
    expect(calcPriority({ ...baseInput, primaryCode: 'K03', toothCount: 2 }).breakdown.value).toBe(5);
    expect(calcPriority({ ...baseInput, primaryCode: 'K03', toothCount: 3 }).breakdown.value).toBe(6);
    expect(calcPriority({ ...baseInput, primaryCode: 'K99', toothCount: 1 }).breakdown.value).toBe(3);
  });

  test('信任基础:lifecycle 底分 + 治疗史/转介/近1年履约各+1,封顶 10', () => {
    const r = calcPriority({
      ...baseInput,
      lifecycleStage: 'mature', // 9
      hasTreatmentHistory: true,
      isReferralChampion: true,
      recentVisitGood: true,
    });
    expect(r.breakdown.trustBase).toBe(10); // 9+3 → 封顶
  });

  test('老诊断 + 低置信来源:新鲜度与置信度因子相乘压低总分', () => {
    const r = calcPriority({ ...baseInput, daysSince: 360, confidence: 0.3 });
    expect(r.breakdown.freshness).toBe(0.4);
    expect(r.breakdown.confidenceFactor).toBe(0.5);
    expect(r.score).toBe(9); // round(4.4875 × 0.4 × 0.5 × 10)
  });

  test('urgency 缺失记 0;满配输入分数不破 100', () => {
    expect(calcPriority({ ...baseInput, urgencyLevel: null }).breakdown.urgency).toBe(0);
    const max = calcPriority({
      ...baseInput,
      urgencyLevel: 'urgent',
      primaryCode: 'K08',
      toothCount: 8,
      rfmSegment: 'important_value',
      consultIntentMatch: true,
      lifecycleStage: 'mature',
      hasTreatmentHistory: true,
      isReferralChampion: true,
      recentVisitGood: true,
    });
    expect(max.score).toBeLessThanOrEqual(100);
    expect(max.score).toBeGreaterThan(90);
  });
});

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

describe('关闭机会 closeReason 覆写(CLOSE_REASON_META.suppressDaysOverride)', () => {
  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('识别不准确 → marked_invalid 自带永久被覆写成 14d 默认档', () => {
    const meta = CLOSE_REASON_META.inaccurate;
    const r = resolveSnoozedUntil({
      outcome: meta.outcome,
      breakerTripped: false,
      now: NOW,
      closeReason: 'inaccurate',
    });
    expect(daysFromNow(r)).toBe(CLOSE_REASON_DEFAULT_SUPPRESS_DAYS);
  });

  test('已完成治疗 / 其他原因 → abandoned 自带 60d 被覆写成 14d', () => {
    for (const reason of ['treated', 'other'] as const) {
      const r = resolveSnoozedUntil({
        outcome: CLOSE_REASON_META[reason].outcome,
        breakerTripped: false,
        now: NOW,
        closeReason: reason,
      });
      expect(daysFromNow(r)).toBe(CLOSE_REASON_DEFAULT_SUPPRESS_DAYS);
    }
  });

  test('无法联系客户 → 覆写成熔断口径 30d', () => {
    const r = resolveSnoozedUntil({
      outcome: CLOSE_REASON_META.unreachable.outcome,
      breakerTripped: false,
      now: NOW,
      closeReason: 'unreachable',
    });
    expect(daysFromNow(r)).toBe(30);
  });

  test('强语义原因无覆写 → 沿用 outcome 自带窗(拒绝 90d / 竞品永久)', () => {
    expect(
      daysFromNow(resolveSnoozedUntil({
        outcome: CLOSE_REASON_META.no_intent.outcome,
        breakerTripped: false, now: NOW, closeReason: 'no_intent',
      })),
    ).toBe(90);
    expect(
      daysFromNow(resolveSnoozedUntil({
        outcome: CLOSE_REASON_META.competitor.outcome,
        breakerTripped: false, now: NOW, closeReason: 'competitor',
      })),
    ).toBe(SUPPRESS_PERMANENT_DAYS);
  });

  test('未知 / 空 closeReason → 不影响原有路径', () => {
    const r = resolveSnoozedUntil({
      outcome: 'refused', breakerTripped: false, now: NOW, closeReason: 'bogus',
    });
    expect(daysFromNow(r)).toBe(90);
    const r2 = resolveSnoozedUntil({ outcome: 'refused', breakerTripped: false, now: NOW });
    expect(daysFromNow(r2)).toBe(90);
  });

  test('熔断优先级仍高于 closeReason 覆写', () => {
    const r = resolveSnoozedUntil({
      outcome: 'abandoned', breakerTripped: true, now: NOW, closeReason: 'treated',
    });
    expect(daysFromNow(r)).toBe(BREAKER_SUPPRESS_DAYS);
  });

  test('每个 closeReason 的 outcome 都是合法 EXECUTION_OUTCOME_META 键', () => {
    for (const meta of Object.values(CLOSE_REASON_META)) {
      expect(EXECUTION_OUTCOME_META[meta.outcome]).toBeDefined();
    }
  });
});
