/**
 * 优先级排序键的精度 —— score 必须与列表展示的 raw 同源同精度
 *
 * 2026-07-29 线上现象:召回池按优先级降序排,客服看到的却是
 * 「3.12 → 3.12 → 3.10 → 3.10 → 3.10 → 3.12 → …」,像没排序。
 *
 * 根因:排序键 `followup_plans.priority_score` 当时是 `Math.round(raw * 10)` —— 0-100 的**整数**,
 * 而列表展示的是 `breakdown.priority.raw` 的**两位小数**(0-10)。排序精度比展示精度粗 10 倍,
 * 同一个整数档里塞着一堆不同的展示分,档内只能按 created_at 兜底排。
 * 测试服实测:priority_score=31 一档里有 21 种展示分(3.20~2.59);
 * 全池 126,073 张工单只有 93 个排序档位,却有 849 种展示分。
 *
 * 不变量:**score === breakdown.raw × 10**。守住它,「按 score 排序」就等价于
 * 「按客服看到的那个数排序」,不会再出现档内乱序。
 */
import { calcPriority, type PriorityInput } from '../src/modules/plan/engine/priority-scorer';

const base: PriorityInput = {
  urgencyLevel: 'mid',
  daysSince: 200,
  windowDays: 180,
  primaryCode: 'K08',
  toothCount: 1,
  rfmSegment: 'general_develop',
  consultIntentMatch: false,
  lifecycleStage: 'reactivate',
  hasTreatmentHistory: false,
  isReferralChampion: false,
  recentVisitGood: false,
  confidence: 1,
};

/** 造一批只在"新鲜度"上有细微差别的输入 —— 正是线上同一档内的那种邻近分 */
const NEIGHBOURS: PriorityInput[] = Array.from({ length: 60 }, (_, i) => ({
  ...base,
  daysSince: 150 + i, // 逐天递增：相邻两档的 raw 只差千分位，正是被旧口径压平的那种
}));

describe('优先级分:排序键与展示值同源', () => {
  test('不变量:score === raw × 10(逐例)', () => {
    for (const input of NEIGHBOURS) {
      const { score, breakdown } = calcPriority(input);
      // 浮点乘法有噪声,比到分厘即可(展示只到两位小数)
      expect(score).toBeCloseTo(breakdown.raw * 10, 6);
    }
  });

  test('按 score 排序 === 按展示值排序(线上那个乱序的正解)', () => {
    const rows = NEIGHBOURS.map((input) => {
      const { score, breakdown } = calcPriority(input);
      return { score, disp: (score / 10).toFixed(2), raw: breakdown.raw };
    });

    const byScore = [...rows].sort((a, b) => b.score - a.score).map((r) => r.disp);
    const byDisp = [...rows].sort((a, b) => Number(b.disp) - Number(a.disp)).map((r) => r.disp);
    expect(byScore).toEqual(byDisp);

    // 且展示序列本身单调不增 —— 客服眼里不会再出现「3.10 之后又冒出 3.12」
    const nums = byScore.map(Number);
    for (let i = 1; i < nums.length; i++) expect(nums[i]!).toBeLessThanOrEqual(nums[i - 1]!);
  });

  test('回归:被旧口径压平的那些分,现在能分开', () => {
    const scored = NEIGHBOURS.map(calcPriority);

    // 找一对「旧实现会判定相等、展示上本来就不同」的 —— 线上乱序就是这种对造成的
    const pair = scored.flatMap((a, i) =>
      scored.slice(i + 1).map((b) => ({ a, b })),
    ).find(({ a, b }) =>
      Math.round(a.score) === Math.round(b.score) && a.breakdown.raw !== b.breakdown.raw,
    );

    expect(pair).toBeDefined(); // 邻近分里必然存在这种对,否则这个 bug 本来就不会发生
    expect(pair!.a.score).not.toBe(pair!.b.score); // 新口径必须分得开（旧实现这里相等）
  });

  test('量纲不变:仍是 0-100,queueStats 的 70/40 分档口径不受影响', () => {
    const top = calcPriority({
      ...base,
      urgencyLevel: 'urgent',
      primaryCode: 'K08',
      toothCount: 3,
      rfmSegment: 'important_value',
      consultIntentMatch: true,
      lifecycleStage: 'mature',
      hasTreatmentHistory: true,
      isReferralChampion: true,
      recentVisitGood: true,
      daysSince: 0,
      confidence: 1,
    });
    expect(top.score).toBeGreaterThan(70);
    expect(top.score).toBeLessThanOrEqual(100);

    const bottom = calcPriority({ ...base, urgencyLevel: null, daysSince: 3000, confidence: 0.5 });
    expect(bottom.score).toBeGreaterThanOrEqual(0);
    expect(bottom.score).toBeLessThan(40);
  });
});
