import { Temperature } from '@pac/types';
import { assertCohortCriteria, cohortWhereSql } from '../src/modules/plan/cohort-filter';

/**
 * 人群取数口径回归。
 *
 * 这一层的错误全都是**静默**的:SQL 照跑、不报错,只是圈出来的人不对。
 * 所以这里逐条锁"生成的 SQL 里必须/不许出现什么" —— 便宜,且拦得住最贵的那几种改法。
 */

const SCOPE = {
  hostId: '00000000-0000-0000-0000-000000000001',
  tenantId: 't1',
  sourceUnits: [] as string[],
  clinicIds: ['c1'],
  userId: 'u1',
} as never;
const NOW = new Date('2026-08-02T00:00:00.000Z');

/** Prisma.Sql 的 strings 拼起来 = 去掉参数占位的 SQL 骨架 */
const sqlOf = (...args: Parameters<typeof cohortWhereSql>) => cohortWhereSql(...args).strings.join('?');

describe('人群取数 —— 池子基线', () => {
  test('⭐ 只圈**未被认领**的活跃 plan,且冷静期已过', () => {
    const s = sqlOf(SCOPE, { clinicId: 'c1' }, NOW);
    expect(s).toContain("fp.status = 'active'");
    expect(s).toContain('fp.assignee_user_id IS NULL');
    expect(s).toContain('fp.superseded_at IS NULL');
    // ⚠️ 约好 6/10 回访的患者不能被卷进批次
    expect(s).toContain('fp.snoozed_until');
  });

  test('⭐ 批次不跨诊所:诊所是硬条件不是可选过滤', () => {
    expect(sqlOf(SCOPE, { clinicId: 'c1' }, NOW)).toContain('fp.target_clinic_id');
  });
});

describe('人群取数 —— 画像必须按患者取当前版', () => {
  const all = sqlOf(
    SCOPE,
    { clinicId: 'c1', potentialTreatment: 'implant', temperature: Temperature.HOT, personaTags: 'rfm:important_value' },
    NOW,
  );

  test('⭐⭐ ⛔ 一处都不许出现 `pe.id = fp.persona_id`(升一次画像版本就静默归零)', () => {
    // 教条 §4.38:plan.persona_id 指向建 plan 那刻的版本,画像升版本后该条件恒为假。
    expect(all).not.toMatch(/pe\.id\s*=\s*fp\.persona_id/);
  });

  test('⭐ 三处(治疗项/温度/画像标签)全部按 patient_id + 当前版关联', () => {
    expect(all.match(/pe\.patient_id = fp\.patient_id/g)?.length).toBe(3);
    expect(all.match(/pe\.superseded_at IS NULL/g)?.length).toBe(3);
  });
});

describe('人群取数 —— 温度', () => {
  test('⭐⭐ 温度必须挂在治疗项上:光给温度直接拒', () => {
    // 同一个人可能「潜在种植·热」而「潜在补牙·冷」,不指定治疗项的"热"是无意义的。
    // ⛔ 尤其不许"贴心地"给个默认治疗项 —— 那会让主管拿到一批他没要的人。
    expect(() => assertCohortCriteria({ clinicId: 'c1', temperature: Temperature.HOT })).toThrow(/必须与潜在治疗项/);
    expect(() => assertCohortCriteria({ clinicId: 'c1', potentialTreatment: 'implant', temperature: Temperature.HOT })).not.toThrow();
  });

  test('⭐⭐ 老画像(无边界)⛔ 不许掉进任何一档 —— 尤其不许默认掉进冷', () => {
    for (const t of [Temperature.HOT, Temperature.WARM, Temperature.COLD]) {
      const s = sqlOf(SCOPE, { clinicId: 'c1', potentialTreatment: 'implant', temperature: t }, NOW);
      // 边界键整个不存在时 `#>` 返回 NULL,这条把它挡在三档之外
      expect(s).toContain('IS NOT NULL');
    }
  });

  test('⭐⭐ 走**稳定路径** temperature.<标签>,⛔ 不走 detail 数组下标', () => {
    // detail 是数组,同一标签在不同患者身上下标不同 → `detail.0.hotUntil` 对谁都不成立,
    // 于是列表页(Prisma json 路径过滤)根本表达不了这个谓词,矩阵与列表就再也对不上数。
    const s = sqlOf(SCOPE, { clinicId: 'c1', potentialTreatment: 'implant', temperature: Temperature.HOT }, NOW);
    expect(s).not.toContain('jsonb_array_elements');
    expect(s).toContain("pf.data #>>");
  });

  test('⭐ 三档互斥且穷尽(有边界的人必进且只进一档)—— 否则矩阵加不出总数', () => {
    const of = (t: (typeof Temperature)[keyof typeof Temperature]) =>
      cohortWhereSql(SCOPE, { clinicId: 'c1', potentialTreatment: 'implant', temperature: t }, NOW);
    // 参数里能看出各档比的是哪个边界:热/冷各一个比较,温两个
    const cmp = (s: ReturnType<typeof of>) => s.strings.join('?').match(/<=|>/g)?.length ?? 0;
    expect(cmp(of(Temperature.HOT))).toBeGreaterThanOrEqual(1);
    expect(cmp(of(Temperature.WARM))).toBeGreaterThan(cmp(of(Temperature.COLD)));
    // 各档引用的边界键:hot→hotUntil / cold→warmUntil / warm→两者
    const keysOf = (t: (typeof Temperature)[keyof typeof Temperature]) =>
      of(t).values.flat().filter((v) => v === 'hotUntil' || v === 'warmUntil');
    expect(keysOf(Temperature.HOT)).toEqual(['hotUntil']);
    expect(keysOf(Temperature.COLD)).toEqual(['warmUntil']);
    expect(keysOf(Temperature.WARM).sort()).toEqual(['hotUntil', 'warmUntil']);
  });

  test('⛔ 温度**不做任何天数运算** —— 天数是时钟,边界才是事实', () => {
    const s = sqlOf(SCOPE, { clinicId: 'c1', potentialTreatment: 'implant', temperature: Temperature.HOT }, NOW);
    expect(s).not.toContain('daysSince');
    expect(s).not.toMatch(/EXTRACT\s*\(\s*DAY/i);
  });
});

describe('人群取数 —— 画像标签', () => {
  test('⭐ 跨维度 AND:两个维度各出一个 EXISTS,不合并成 OR', () => {
    const s = sqlOf(SCOPE, { clinicId: 'c1', personaTags: 'rfm:important_value,gender:male' }, NOW);
    expect(s.match(/AND EXISTS \(/g)?.length).toBe(2);
  });

  test('⭐ 非法维度 / 非法取值静默丢弃(与列表页同行为,不报错)', () => {
    const bogus = sqlOf(SCOPE, { clinicId: 'c1', personaTags: 'not_a_dim:x,rfm:not_a_value' }, NOW);
    const none = sqlOf(SCOPE, { clinicId: 'c1' }, NOW);
    expect(bogus).toBe(none);
  });

  test('⭐ 数组维度走 @> 包含,标量维度走等值 —— 用错索引吃不到,生产实测会退化到 20 秒', () => {
    expect(sqlOf(SCOPE, { clinicId: 'c1', personaTags: 'entitlement_status:medical' }, NOW)).toContain('@>');
    expect(sqlOf(SCOPE, { clinicId: 'c1', personaTags: 'gender:male' }, NOW)).toContain('pf.data ->>');
  });

  test('⭐ 「上次到诊」走 patient_profiles 的日期区间,不穿画像表', () => {
    const s = sqlOf(SCOPE, { clinicId: 'c1', personaTags: 'last_visit_bucket:0_3m' }, NOW);
    expect(s).toContain('patient_profiles');
    expect(s).toContain('pp.last_visit_at');
  });
});
