import { classifyCodeToLabel, EXTRACTION_NAME_KEYWORDS, POTENTIAL_LABEL_RULES } 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 { labelCaseSql, windowValuesSql } 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*\?/);
  });

  test('⭐⭐ 窗口天数来自 DiagnosisTreatmentMap(参数注入),⛔ 不是 SQL 里的字面量', () => {
    const w = windowValuesSql();
    const wflat = w.strings.join('?');
    // K08 种植 120/180、K02 龋 60/90 —— 必须是参数
    for (const n of [120, 180, 60, 90]) expect(w.values.flat()).toContain(n);
    expect(wflat).not.toMatch(/\b(120|180)\b/);
  });

  test('⭐ 只生成 8 类标签用到的码 —— K00/K09 不该出现(它们没有矩阵行)', () => {
    const codes = windowValuesSql().values.flat().filter((v) => typeof v === 'string');
    expect(codes).not.toContain('K00');
    expect(codes).not.toContain('K09');
    expect(codes).toContain('K08');
  });
});
