/**
 * 治疗术式关键词分类器 — CI 防漂移测试。
 *
 * 验证:精确 enum_mapping 未命中时,keyword_mapping(按序含词 + 条件从句剥离)能把
 *       host 长尾 free-text 术式正确归到 PAC category;噪音/无操作正确落空(→ _default 丢弃)。
 *
 * fixture 术式取自服务器真实漏配样本(/tmp/sync-full2.log missing-category WARN)。
 * 直接加载线上 jvs-dw yaml 的 keyword_mapping(顺带校验新 schema 字段合法)。
 */
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'js-yaml';
import { classifyByKeyword } from '../src/modules/sync/assembler/assembler-engine';
import { AssemblerConfigSchema } from '../src/modules/sync/assembler/assembler.schema';
import { resolveDictIncludes } from '../src/modules/sync/assembler/dict-include';

function loadCfg(file: string) {
  const p = path.join(__dirname, '..', 'data', 'jvs-dw', 'assemblers', file);
  // 走生产同款加载:dict_includes(共享字典分层)在 zod 校验前解析合并
  const parsed = resolveDictIncludes(yaml.load(fs.readFileSync(p, 'utf-8')), p);
  const r = AssemblerConfigSchema.safeParse(parsed);
  if (!r.success) {
    throw new Error(`${file} schema invalid:\n${JSON.stringify(r.error.issues, null, 2)}`);
  }
  return r.data;
}

describe('treatment_actual keyword classifier (real jvs-dw yaml)', () => {
  const cfg = loadCfg('treatment_actual.yaml');
  const rules = cfg.keyword_mapping!.category!;
  const strip = cfg.keyword_strip_clauses;

  // [术式(已 normalize:ASCII 标点), 期望 category 或 undefined(落 _default), 解决的问题类]
  const cases: Array<[string, string | undefined, string]> = [
    ['洁牙+喷砂', 'periodontic', 'A 加号复合'],
    ['龈上洁治+局部刮治', 'periodontic', 'A'],
    ['龈上洁治术+喷砂', 'periodontic', 'A 后缀变体(术)'],
    ['根管治疗,冠修复', 'endodontic', 'B 复合优先级 根管>冠'],
    ['种植冠/桥修复(多颗)', 'implant', 'B 种植>冠'],
    ['根管治疗+桩冠修复', 'endodontic', 'B'],
    ['洗牙', 'periodontic', 'C 同义口语'],
    ['细洁', 'periodontic', 'C'],
    ['定期涂氟', 'preventive', 'C'],
    ['拟涂氟,已告知家长及患者病情', 'preventive', 'D 剥离备注噪音(已告知)'],
    ['窝沟封闭。已告知家长', 'preventive', 'D'],
    ['充填,必要时根管治疗', 'restorative', 'E 负向词:剥离"必要时根管"→充填'],
    ['戴贴面', 'cosmetic', '贴面先于冠'],
    ['乳牙拔除术', 'surgical', '操作词(拔除)>人群词(乳牙)'],
    ['儿童早期矫治', 'orthodontic', '操作词(矫治)>人群词(儿童)'],
    ['活动义齿修复', 'prosthodontic', ''],
    ['全瓷冠修复', 'prosthodontic', ''],
    // —— 噪音 / 无操作 → undefined(落 _default 丢弃,不误判成治疗)——
    ['定期观察,必要时拔除', undefined, 'E:剥离"必要时拔除"→只剩观察→不误判 surgical'],
    ['无需处理', undefined, '无操作'],
    ['不予处理(无间隙)', undefined, '无操作'],
    ['未指定治疗项', undefined, '无操作'],
    ['拍照口扫,反馈给加工单位,调整治疗计划', undefined, '流程'],
  ];

  test.each(cases)('「%s」→ %s', (input, expected) => {
    expect(classifyByKeyword(input, rules, strip)).toBe(expected);
  });

  it('keyword_mapping/strip 字段被 schema 接受且非空', () => {
    expect(rules.length).toBeGreaterThan(5);
    expect(strip).toContain('必要时');
  });
});

describe('treatment_planned keyword classifier (计划:不剥建议/拟)', () => {
  const cfg = loadCfg('treatment_planned.yaml');
  const rules = cfg.keyword_mapping!.category!;
  const strip = cfg.keyword_strip_clauses;

  it('「建议拔除智齿」→ surgical(planned 保留"建议")', () => {
    expect(classifyByKeyword('建议拔除智齿', rules, strip)).toBe('surgical');
  });

  it('「建议全口洁治后,必要时正畸治疗」→ periodontic(剥"必要时正畸"留洁治)', () => {
    expect(classifyByKeyword('建议全口洁治后,必要时正畸治疗', rules, strip)).toBe('periodontic');
  });

  it('planned strip 不含"建议"', () => {
    expect(strip).not.toContain('建议');
    expect(strip).toContain('必要时');
  });

  // ─────────────────────────────────────────────────────────
  // 2026-07 线上反馈(陈芃霖 BJ0A094257):裸「二期」误吞正畸。
  //   修法 = 把裸「二期」从 implant 主规则降到全表最末兜底(不是收紧成「种植二期」——
  //   planned 侧的种植二期普遍不带"种植",收紧会丢 900+ 条,见 yaml 头注释)。
  //   下面两组用例把这个**位次**锁死:动了顺序就红。
  // ─────────────────────────────────────────────────────────
  describe('裸「二期」位次(正畸先收 / 种植兜底)', () => {
    // 组一:带正畸词 → 必须 orthodontic(prod 实测 420 条 planned 属这组)
    const orthoCases = [
      '正畸二期矫治', // ⭐ 线上原文:2025-12-20 的计划
      '二期矫治',
      '二期矫正',
      '正畸二期',
      '正畸二期治疗',
      '早期矫正+二期矫正',
      '早期矫治+二期矫治',
      '酌情二期矫治',
      '恒牙列期酌情二期矫治',
      '二期矫治(大概率拔牙)',
    ];
    it.each(orthoCases)('「%s」→ orthodontic(不再被种植吞)', (name) => {
      expect(classifyByKeyword(name, rules, strip)).toBe('orthodontic');
    });

    // 组二:无正畸词 → 仍 implant(修复前后逐条一致,不能因为改顺序把它们丢了)
    const implantCases = [
      '二期取模', // prod 346 条,planned 里最大的一组
      '二期手术',
      '二期修复',
      '二期',
      '三个月后二期',
      '3个月后二期手术',
      '4-6个月后二期修复',
      '二期口扫',
    ];
    it.each(implantCases)('「%s」→ implant(种植二期常省"种植",靠末位兜底)', (name) => {
      expect(classifyByKeyword(name, rules, strip)).toBe('implant');
    });

    it('真种植计划不受影响(含"种植"走第 1 条,不依赖兜底)', () => {
      expect(classifyByKeyword('种植修复', rules, strip)).toBe('implant');
      expect(classifyByKeyword('正畸后种植修复', rules, strip)).toBe('implant');
      expect(classifyByKeyword('种植二期', rules, strip)).toBe('implant');
    });

    it('⭐ 结构约束:裸「二期」规则必须排在 orthodontic 之后(挪回文件头即误吞)', () => {
      const bareErqi = rules.findIndex((r) => r.value === 'implant' && r.any.includes('二期'));
      const ortho = rules.findIndex((r) => r.value === 'orthodontic');
      expect(bareErqi).toBeGreaterThan(-1);
      expect(ortho).toBeGreaterThan(-1);
      expect(bareErqi).toBeGreaterThan(ortho);
      // 且 implant 主规则(种植/植体)仍在最前,不能被降位
      expect(rules.findIndex((r) => r.value === 'implant' && r.any.includes('种植'))).toBeLessThan(ortho);
    });
  });
});
