import {
  Temperature,
  classifyTemperature,
  gapTemperatureBounds,
  hottestBounds,
  lookupDxTreatment,
} from '@pac/types';
import { VOLATILE_DATA_KEYS } from '../src/modules/persona/persona-diff';

/**
 * 窗口温度口径回归(教条 T6 / 开发规划 Q-6 的落地)。
 *
 * 这里锁的是三件**做错了不会报错、只会让主管看到一个假分布**的事:
 *   ① 逐条 gap 按自己 K 码的窗判档 —— 而不是先 max(daysSince) 再判
 *   ② 边界时刻是事实不是时钟 —— 换个"现在"重算,值必须一模一样
 *   ③ 老数据没有边界时 → 「未知」,⛔ 不许当成「冷」
 */

const DAY = 86_400_000;
const NOW = new Date('2026-08-02T00:00:00.000Z');
const daysAgo = (n: number) => new Date(NOW.getTime() - n * DAY);

describe('温度口径 —— 逐条 gap 用自己的窗', () => {
  test('⭐ 各码的边界严格来自 DiagnosisTreatmentMap,不是自己拍的常量', () => {
    for (const code of ['K01', 'K02', 'K03', 'K04', 'K05', 'K06', 'K07', 'K08']) {
      const rule = lookupDxTreatment(code)!;
      const b = gapTemperatureBounds(code, daysAgo(0))!;
      expect(new Date(b.hotUntil).getTime() - NOW.getTime()).toBe(rule.urgencyDayThreshold * DAY);
      expect(new Date(b.warmUntil).getTime() - NOW.getTime()).toBe(rule.windowDays * DAY);
    }
  });

  test('⭐⭐ Q-6「一对多无解」其实有解:extraction ← K01(180/90) + K03(90/60) 各归各的', () => {
    // 同一个「潜在拔牙」标签下的两条 gap,天数一样(70 天),但两码的黄金期不同:
    //   K01 urgency=90 → 70 天仍在黄金期 = 热
    //   K03 urgency=60 → 70 天已过黄金期 = 温
    // 取最热 → 热。⛔ 不需要为「潜在拔牙」另定一套标签级窗口(那才是新增口径)。
    const k01 = gapTemperatureBounds('K01', daysAgo(70));
    const k03 = gapTemperatureBounds('K03', daysAgo(70));
    expect(classifyTemperature(k01, NOW)).toBe(Temperature.HOT);
    expect(classifyTemperature(k03, NOW)).toBe(Temperature.WARM);
    expect(classifyTemperature(hottestBounds([k01, k03]), NOW)).toBe(Temperature.HOT);
  });

  test('⭐ perio ← K05(120/90) + K06(120/60) 同理', () => {
    const k05 = gapTemperatureBounds('K05', daysAgo(75));
    const k06 = gapTemperatureBounds('K06', daysAgo(75));
    expect(classifyTemperature(k05, NOW)).toBe(Temperature.HOT); // 75 ≤ 90
    expect(classifyTemperature(k06, NOW)).toBe(Temperature.WARM); // 75 > 60
    expect(classifyTemperature(hottestBounds([k05, k06]), NOW)).toBe(Temperature.HOT);
  });

  test('三档边界是闭区间左闭右闭:恰好落在窗上的那天仍算窗内', () => {
    expect(classifyTemperature(gapTemperatureBounds('K02', daysAgo(60)), NOW)).toBe(Temperature.HOT); // urgency=60
    expect(classifyTemperature(gapTemperatureBounds('K02', daysAgo(61)), NOW)).toBe(Temperature.WARM);
    expect(classifyTemperature(gapTemperatureBounds('K02', daysAgo(90)), NOW)).toBe(Temperature.WARM); // window=90
    expect(classifyTemperature(gapTemperatureBounds('K02', daysAgo(91)), NOW)).toBe(Temperature.COLD);
  });

  test('未知码 / 无锚点 → null,⛔ 不猜一个默认窗口', () => {
    expect(gapTemperatureBounds('K99', daysAgo(1))).toBeNull();
    expect(gapTemperatureBounds('K02', null)).toBeNull();
    expect(gapTemperatureBounds('K02', 'not-a-date')).toBeNull();
  });
});

describe('温度口径 —— 反单调性(这是旧口径最贵的那个 bug)', () => {
  test('⭐⭐ 多一条**更旧**的同类需求,绝不能让人变冷', () => {
    // 上周刚查出的龋齿(K02,5 天)+ 两年前的旧龋(K02,700 天)。
    // 旧口径:max(daysSince)=700 → 冷,这个刚被医生说过的人在矩阵上永远看不见。
    const fresh = gapTemperatureBounds('K02', daysAgo(5));
    const stale = gapTemperatureBounds('K02', daysAgo(700));
    expect(classifyTemperature(fresh, NOW)).toBe(Temperature.HOT);
    expect(classifyTemperature(stale, NOW)).toBe(Temperature.COLD);
    expect(classifyTemperature(hottestBounds([fresh, stale]), NOW)).toBe(Temperature.HOT);
    expect(classifyTemperature(hottestBounds([stale, fresh]), NOW)).toBe(Temperature.HOT); // 与顺序无关
  });

  test('⭐ 单调性通则:往集合里加任何一条 gap,档位只可能变热或不变', () => {
    const base = [gapTemperatureBounds('K02', daysAgo(700))];
    const rank = { hot: 0, warm: 1, cold: 2 } as const;
    const before = rank[classifyTemperature(hottestBounds(base), NOW)!];
    for (const d of [1, 30, 59, 60, 61, 89, 90, 91, 365, 3000]) {
      const after = rank[classifyTemperature(hottestBounds([...base, gapTemperatureBounds('K02', daysAgo(d))]), NOW)!];
      expect(after).toBeLessThanOrEqual(before);
    }
  });

  test('⭐ 不变式 hotUntil < warmUntil 在跨码取 max 之后仍成立', () => {
    // 两个 max 可以来自不同 gap,这里证明那不会把区间弄反(弄反 = 温档凭空消失)
    const mixed = hottestBounds([
      gapTemperatureBounds('K01', daysAgo(10)), // hot 到 +80d, warm 到 +170d
      gapTemperatureBounds('K04', daysAgo(1)), // hot 到 +44d, warm 到 +59d
    ])!;
    expect(mixed.hotUntil < mixed.warmUntil).toBe(true);
  });
});

describe('温度口径 —— 边界时刻是事实,不是时钟', () => {
  test('⭐⭐ 换个"现在"重算,边界一字不变(否则每天都白升一个画像版本)', () => {
    const anchor = new Date('2026-01-15T03:00:00.000Z');
    const a = gapTemperatureBounds('K08', anchor);
    const b = gapTemperatureBounds('K08', anchor); // 同样的事实,隔了半年再算
    expect(a).toEqual(b);
  });

  test('⭐⭐ hotUntil / warmUntil ⛔ 不许进 VOLATILE_DATA_KEYS', () => {
    // 进了 = stripVolatile 会把它们从语义指纹里剔掉 → 新来一条诊断把温度顶热了也不升版本,
    // 等于把这次要修的"冻结"原样搬到另一层,而且更隐蔽。
    expect(VOLATILE_DATA_KEYS.has('hotUntil')).toBe(false);
    expect(VOLATILE_DATA_KEYS.has('warmUntil')).toBe(false);
    // 对照:daysSince 是真·易变键,必须在里面(它是时钟)
    expect(VOLATILE_DATA_KEYS.has('daysSince')).toBe(true);
  });

  test('⭐ 时间推移会自然翻档,不需要任何重算', () => {
    const b = gapTemperatureBounds('K04', new Date('2026-08-02T00:00:00.000Z'))!; // 60/45
    expect(classifyTemperature(b, new Date('2026-09-10T00:00:00.000Z'))).toBe(Temperature.HOT); // +39d
    expect(classifyTemperature(b, new Date('2026-09-20T00:00:00.000Z'))).toBe(Temperature.WARM); // +49d
    expect(classifyTemperature(b, new Date('2026-11-01T00:00:00.000Z'))).toBe(Temperature.COLD); // +91d
  });

  test('⭐ 边界存的是 UTC ISO —— 字典序必须等于时间序(SQL 侧靠这个直接比较)', () => {
    const early = gapTemperatureBounds('K02', new Date('2026-01-01T00:00:00.000Z'))!;
    const late = gapTemperatureBounds('K02', new Date('2026-06-01T00:00:00.000Z'))!;
    expect(early.hotUntil < late.hotUntil).toBe(true);
    expect(early.hotUntil).toMatch(/Z$/); // 带 +08:00 偏移的写法会破坏字典序
  });
});

describe('温度口径 —— 「不知道」不等于「冷」', () => {
  test('⭐⭐ 老画像没有边界 → null,⛔ 不许静默判成 cold', () => {
    // 判成 cold 会让本次改动之前算出来的画像全部塞进冷格子,
    // 主管看到的是一个**假的**分布,而且看不出哪里假 —— 违 T14。
    expect(classifyTemperature(null, NOW)).toBeNull();
    expect(classifyTemperature(undefined, NOW)).toBeNull();
    expect(classifyTemperature({}, NOW)).toBeNull();
    expect(classifyTemperature({ hotUntil: '2026-09-01T00:00:00.000Z' }, NOW)).toBeNull(); // 只有一半也不算
  });

  test('一条 gap 都算不出边界时,聚合结果也是 null', () => {
    expect(hottestBounds([])).toBeNull();
    expect(hottestBounds([null, undefined])).toBeNull();
  });
});
