import {
  personaWriteVerdict,
  VOLATILE_DATA_KEYS,
  type StoredPersonaFeature,
} from '../src/modules/persona/persona-diff';
import type { PersonaFeatureDraft } from '../src/modules/persona/features/feature.interface';

/**
 * 画像写入判定回归。
 *
 * 背景(生产事故):画像读 patient_facts,stale 判定却只看 patient_transactions 的水位。
 * 2026-07-21「amount_cents 应收→实付」reparse 只改事实、不产事务 → 之后的全量重算
 * 被水位闸 noop 掉 325,979 人,74% 存量画像的累计消费永久停在旧口径(中位高估 91%),
 * 而这个数会去和实时算的 M 分位阈值比大小 → mScore 系统性偏高 → 召回优先级被抬。
 *
 * 修法是把水位对齐到 fact 层。闸放宽之后「算了但没变」变多,本模块是**写入侧的闸**:
 *   unchanged 一字未变 → 不写 feature 行
 *   volatile  只有时间派生值变 → 就地刷新,不升版本
 *   semantic  真变了 → 升版本
 *
 * 本文件同时守住两条红线:
 *   ① 不能把"事实变才变"的键(monetaryCents 等)误判成易变 —— 那就等于没修事故
 *   ② 不能把纯时间漂移judg成语义变化 —— 那会让每次重算都白升一版
 */

const draft = (over: Partial<PersonaFeatureDraft> = {}): PersonaFeatureDraft =>
  ({
    key: 'rfm',
    description: '重要价值 · 距上次 140 天 · 就诊20次 · 累计¥154,350',
    score: null,
    data: {
      segment: 'important_value',
      rScore: 5,
      fScore: 5,
      mScore: 5,
      recencyDays: 140,
      firstSeenDays: 3000,
      freqCount: 20,
      monetaryCents: 15435000,
      valueTier: 4,
      riskScore: 2,
      hasTreatmentGap: true,
    },
    evidence: { factIds: ['f1', 'f2'] },
    ...over,
  }) as PersonaFeatureDraft;

/** draft → 库里那行的形状(字段同名,类型放宽) */
const stored = (d: PersonaFeatureDraft): StoredPersonaFeature => ({
  key: d.key,
  description: d.description,
  score: d.score ?? null,
  data: d.data ?? null,
  evidence: d.evidence,
});

const verdict = (cur: PersonaFeatureDraft[], next: PersonaFeatureDraft[]) =>
  personaWriteVerdict(cur.map(stored), next);

describe('personaWriteVerdict — 三档判定', () => {
  test('⭐ 完全相同 → unchanged(不写任何 feature 行)', () => {
    expect(verdict([draft()], [draft()])).toBe('unchanged');
  });

  test('⭐ 只有天数漂移(recencyDays 140→151)→ volatile,**不升版本**', () => {
    const next = draft({
      description: '重要价值 · 距上次 151 天 · 就诊20次 · 累计¥154,350',
      data: { ...(draft().data as object), recencyDays: 151 },
    });
    expect(verdict([draft()], [next])).toBe('volatile');
  });

  test('⭐⭐ 累计消费变了(应收→实付,正是事故本体)→ semantic,必须升版本', () => {
    const next = draft({
      description: '重要价值 · 距上次 140 天 · 就诊20次 · 累计¥195,750',
      data: { ...(draft().data as object), monetaryCents: 19575000 },
    });
    expect(verdict([draft()], [next])).toBe('semantic');
  });

  test('⭐ 分群变了(mScore 掉档导致 segment 变)→ semantic', () => {
    const next = draft({
      data: { ...(draft().data as object), segment: 'general_value', mScore: 3 },
    });
    expect(verdict([draft()], [next])).toBe('semantic');
  });

  test('首次(无当前版本)→ semantic', () => {
    expect(personaWriteVerdict(null, [draft()])).toBe('semantic');
    expect(personaWriteVerdict([], [draft()])).toBe('semantic');
  });

  test('特征全没了(从有到无)→ semantic', () => {
    expect(verdict([draft()], [])).toBe('semantic');
  });

  test('新增一个特征 → semantic', () => {
    const extra = draft({ key: 'age_bracket', data: { bracket: 'senior', label: '老年', ageYears: 58 } });
    expect(verdict([draft()], [draft(), extra])).toBe('semantic');
  });

  test('掉了一个特征(降级为不打标签)→ semantic', () => {
    const extra = draft({ key: 'age_bracket', data: { bracket: 'senior', label: '老年', ageYears: 58 } });
    expect(verdict([draft(), extra], [draft()])).toBe('semantic');
  });

  test('特征顺序不同 → 不算变化(库里/代码里顺序本就不保证)', () => {
    const a = draft({ key: 'age_bracket', data: { bracket: 'senior' } });
    expect(verdict([draft(), a], [a, draft()])).toBe('unchanged');
  });
});

describe('易变键清单 —— 时间派生值不该升版本', () => {
  // 判据:拿同一批事实、换个时刻重算,值就会变
  test.each([
    ['recencyDays', 'rfm / lifecycle_stage 距末诊天数'],
    ['firstSeenDays', 'rfm / lifecycle_stage 距首诊天数'],
    ['lastVisitDays', 'urgency_level 距末诊天数'],
    ['ageYears', 'age_bracket 周岁'],
    ['visitsPerYear', 'lifecycle_stage 年均就诊(时间做分母)'],
    ['netThisCents', 'lifecycle_stage 近 1 年净消费(滑窗)'],
    ['fulfillRate', 'special_attention 近 1 年履约率(滑窗)'],
    ['recordCount', 'time_preference 近 2 年预约数(滑窗)'],
  ])('%s 变化 → volatile(%s)', (key) => {
    const base = draft({ data: { segment: 'important_value', [key]: 1 } });
    const next = draft({ data: { segment: 'important_value', [key]: 999 }, description: '改了' });
    expect(verdict([base], [next])).toBe('volatile');
  });

  test('⭐ 嵌套里的 daysSince 也要剔(potential_treatment.detail[])', () => {
    const mk = (d: number) =>
      draft({
        key: 'potential_treatment',
        description: '潜在种植 / 潜在修复',
        data: {
          types: ['implant', 'restoration'],
          labels: ['潜在种植', '潜在修复'],
          detail: [{ key: 'implant', teeth: ['16'], daysSince: d, confidence: 1 }],
        },
      });
    expect(verdict([mk(140)], [mk(151)])).toBe('volatile');
  });

  test('⭐ 红线:牙位变了(detail[].teeth)→ semantic,不能被 detail 的剔除逻辑连坐', () => {
    const mk = (teeth: string[]) =>
      draft({
        key: 'potential_treatment',
        data: { types: ['implant'], detail: [{ key: 'implant', teeth, daysSince: 140 }] },
      });
    expect(verdict([mk(['16'])], [mk(['16', '17'])])).toBe('semantic');
  });

  test('⭐ 红线:清单里不能混进"事实变才变"的键', () => {
    for (const k of ['monetaryCents', 'netTotalCents', 'totalVisits', 'freqCount', 'types', 'segment', 'stage', 'level', 'bracket']) {
      expect(VOLATILE_DATA_KEYS.has(k)).toBe(false);
    }
  });
});

describe('evidence 维度', () => {
  test('证据集变了(命中新 fact)→ semantic', () => {
    const next = draft({ evidence: { factIds: ['f1', 'f2', 'f3'] } });
    expect(verdict([draft()], [next])).toBe('semantic');
  });

  test('⭐ factIds 只是顺序不同 → 不算变化(批次间顺序会抖)', () => {
    const next = draft({ evidence: { factIds: ['f2', 'f1'] } });
    expect(verdict([draft()], [next])).toBe('unchanged');
  });

  test('evidence 形状异常不炸', () => {
    const a = draft({ evidence: null as never });
    const b = draft({ evidence: {} as never });
    expect(verdict([a], [b])).toBe('unchanged');
  });
});

describe('⭐ 红线:jsonb 键序不能被当成变化', () => {
  // Postgres jsonb 不保留键序 —— 不规范化的话每次重算都判"变了",全量重写(3.1 GB 表)
  test('顶层键序不同 → unchanged', () => {
    const asStored = stored(draft());
    asStored.data = {
      hasTreatmentGap: true, monetaryCents: 15435000, valueTier: 4, segment: 'important_value',
      riskScore: 2, freqCount: 20, firstSeenDays: 3000, recencyDays: 140,
      mScore: 5, fScore: 5, rScore: 5,
    };
    expect(personaWriteVerdict([asStored], [draft()])).toBe('unchanged');
  });

  test('嵌套对象键序不同 → unchanged', () => {
    const mk = (o: Record<string, unknown>) =>
      draft({ key: 'potential_treatment', data: { detail: [o] } });
    const a = mk({ key: 'implant', teeth: ['16'], confidence: 1 });
    const b = mk({ confidence: 1, teeth: ['16'], key: 'implant' });
    expect(verdict([a], [b])).toBe('unchanged');
  });

  test('数组顺序仍算变化(types 首项 = 主推,有序)', () => {
    const a = draft({ key: 'treatment_history', data: { types: ['implant_history', 'perio_history'] } });
    const b = draft({ key: 'treatment_history', data: { types: ['perio_history', 'implant_history'] } });
    expect(verdict([a], [b])).toBe('semantic');
  });
});

describe('红线:判定不依赖 description 文案', () => {
  test('⭐ data 相同、只有文案模板改了 → volatile(就地刷新),不升版本', () => {
    const next = draft({ description: '【重要价值】140 天未到诊,累计 154,350 元' });
    expect(verdict([draft()], [next])).toBe('volatile');
  });
});
