import {
  classifyCodeToLabel,
  EXTRACTION_NAME_KEYWORDS,
  HOT_BUCKET_DAYS,
  POTENTIAL_LABEL_RULES,
  WARM_BUCKET_DAYS,
} from '@pac/types';
import { classifyGapToLabel } from '../src/modules/persona/features/potential-treatment.feature';
import type { PotentialGap } from '../src/modules/clinical-gap/potential-treatment.selector';
import {
  AGE_YEARS_SQL,
  labelCaseSql,
  labelTemperatureExistsSql,
} from '../src/modules/plan/reason-temperature.sql';

/**
 * 🔴 码 → 业务标签的判定,现在有**两个执行现场**:
 *   · TS 侧 `classifyGapToLabel`(画像出标签、urgency_level 判待转)
 *   · SQL 侧 `labelCaseSql`(初选矩阵 / 确认单 / 列表,2026-08 换成 reason 驱动后新增)
 *
 * 两边分叉的表现是「矩阵里有这个人、点进去列表没有」—— T6a 明令要防的那件事,
 * **而且不报错**。所以这里在**全量组合**上逐个比对,不是抽样。
 *
 * 声明式规则表 `POTENTIAL_LABEL_RULES` 是给 SQL 生成用的;本文件证明它与那个 switch 等价。
 */

const CODES = ['K00', 'K01', 'K02', 'K03', 'K04', 'K05', 'K06', 'K07', 'K08', 'K09', 'UNKNOWN'];
const AGES = [null, 0, 2, 3, 12, 13, 18, 19, 40, 41, 80];
const NAMES = ['', '牙列缺损', '残根', '残冠伴龋', '无法保留', '不能保留的患牙', '楔状缺损'];

const gapOf = (code: string, nameZh: string): PotentialGap =>
  ({ primaryCode: code, nameZh, factId: 'f', code, tooth: null, daysSince: 0, anchorAt: new Date(), signalType: 'diagnosis', confidence: 1 }) as unknown as PotentialGap;

describe('规则表与 classifyGapToLabel 等价(全量组合)', () => {
  test('⭐⭐ 11 码 × 11 年龄 × 7 诊断名 = 847 组,逐个一致', () => {
    const diffs: string[] = [];
    for (const code of CODES) {
      for (const age of AGES) {
        for (const name of NAMES) {
          const fromSwitch = classifyGapToLabel(gapOf(code, name), age)?.key ?? null;
          const fromTable = classifyCodeToLabel(code, name, age);
          if (fromSwitch !== fromTable) {
            diffs.push(`${code}/age=${age}/"${name}": switch=${fromSwitch} table=${fromTable}`);
          }
        }
      }
    }
    expect(diffs).toEqual([]);
  });

  test('⭐ 关键词只有一份 —— switch 用的就是 @pac/types 那个常量', () => {
    // 改了关键词而只改一处 = 两边分叉。这里断言它们**是同一个数组引用来源**的内容。
    expect(EXTRACTION_NAME_KEYWORDS).toContain('残根');
    expect(classifyCodeToLabel('K03', '残根', 30)).toBe('extraction');
    expect(classifyCodeToLabel('K03', '楔状缺损', 30)).toBe('restoration');
  });
});

describe('SQL 生成 —— 数字/关键词一个都不许手抄', () => {
  const sql = labelCaseSql('f', { strings: ['age'], values: [] } as never);
  const flat = sql.strings.join('?');

  test('⭐⭐ CASE 的 WHEN 条数 == 规则表条数(漏一条 = 那类患者静默消失)', () => {
    expect((flat.match(/WHEN/g) ?? []).length).toBe(POTENTIAL_LABEL_RULES.length);
  });

  test('⭐⭐ K03 的「含关键词→拔牙」必须排在「→修复」之前(顺序敏感)', () => {
    // 调换顺序会让所有 K03 都归修复,而 SQL 完全合法、跑得通。
    const vals = sql.values.flat();
    expect(vals.indexOf('extraction')).toBeLessThan(vals.indexOf('restoration'));
  });

  test('⭐ 关键词以**参数**形式注入,⛔ 不拼进 SQL 文本(业务配置可能含引号/元字符)', () => {
    for (const k of EXTRACTION_NAME_KEYWORDS) {
      expect(flat).not.toContain(k);
      expect(sql.values.flat()).toContain(k);
    }
  });

  test('⭐ 关键词判包含用 position() 而不是正则 —— 关键词是业务配置,可能含正则元字符', () => {
    expect(flat).toContain('position(');
    expect(flat).not.toMatch(/~\s*\?/);
  });

  /**
   * 🔴 2026-08-11:原来这里锁的是 `windowValuesSql()`(按码的 `(code, urg, wnd)` VALUES 表)——
   *   前两档改成固定 90/180 天后那张表连同它的 JOIN 一起删了,这两条用例随之改写成:
   *   ① 天数仍走参数注入(常量在 @pac/types,SQL 文本里不许出现字面量);
   *   ② 删掉那次 JOIN **没有放宽行集**(它当年顺带起的过滤作用由 labelCase 全额承担)。
   */
  test('⭐⭐ 前两档天数走参数注入,⛔ 不是 SQL 里的字面量', () => {
    const s = labelTemperatureExistsSql('implant', ['hot']);
    expect(s.values.flat()).toContain(HOT_BUCKET_DAYS);
    expect(s.values.flat()).toContain(WARM_BUCKET_DAYS);
    expect(s.strings.join('?')).not.toMatch(/\b(90|180)\b/);
  });

  test('⭐ 码白名单仍只认 8 类标签的码 —— K00/K09 落 NULL(它们没有矩阵行)', () => {
    // 删 JOIN 的等价性就落在这条:过滤现在**只**由 labelCase 做,
    // 而它认的码集合与当年那张 VALUES 表逐字相同。
    const codes = [...new Set(POTENTIAL_LABEL_RULES.map((r) => r.code))];
    expect(codes).not.toContain('K00');
    expect(codes).not.toContain('K09');
    expect(codes).toContain('K08');
    expect(labelCaseSql('f', AGE_YEARS_SQL).values.flat()).not.toContain('K00');
  });
});
