import {
  DENTURE_FIRST_AGE,
  IMPLANT_LAST_AGE,
  EARLY_ORTHO_MAX_AGE,
  refineCategoriesForDiagnosis,
  recommendedCategoriesForAge,
  treatmentCategoryNameZhFor,
  treatmentCategoryNameZhForTeeth,
  lookupDxTreatment,
} from '@pac/types';

/**
 * 建议治疗的年龄适配回归(源头层:诊断 → 建议治疗的**排序**)。
 *
 * 背景(2026-07 业务反馈):90 岁 / 78 岁患者的缺牙召回,界面目标一律「目标 · 种植」——
 * 因为 UI 取 expectedCategories[0],而 K08 的 categories 首项恒为 implant。
 *
 * 业务方口径(对齐后):**70-80 不是种植禁忌**,能否种植看骨量骨质 / 全身状况 / 患者意愿,
 * 由医生评估。PAC 该做的是**调整推荐顺序**(活动义齿在前、种植并行),不是禁推种植。
 *
 * 设计边界(三条红线,下面各有用例锁住):
 *   1. 只改顺序,不增删类目 —— 返回集合恒等于入参集合(种植不能被抹掉)
 *   2. 不碰排除闸(DiagnosisTreatmentMap[].categories 仍是完整候选集)
 *   3. 不碰"要不要召回"(缺牙影响咀嚼营养,高龄同样该管)
 */

describe('recommendedCategoriesForAge — 源头:建议治疗排序', () => {
  const K08 = lookupDxTreatment('K08')!.categories; // ['implant','prosthodontic']

  test('⭐ >70 岁缺牙 → 活动义齿在前,种植并行保留(不是二选一)', () => {
    const r = recommendedCategoriesForAge(K08, 90);
    expect(r[0]).toBe('prosthodontic'); // 义齿在前
    expect(r).toContain('implant'); // ⭐ 种植仍在 —— 高龄非禁忌,只是排后面
    expect(r).toHaveLength(K08.length); // 只挪位,不增删
  });

  test('边界:恰好 70 岁不变(口径是「大于 70」)', () => {
    expect(recommendedCategoriesForAge(K08, DENTURE_FIRST_AGE)[0]).toBe('implant');
    expect(recommendedCategoriesForAge(K08, DENTURE_FIRST_AGE + 1)[0]).toBe('prosthodontic');
  });

  test('未达线 / 无年龄 → 原顺序', () => {
    expect(recommendedCategoriesForAge(K08, 35)[0]).toBe('implant');
    expect(recommendedCategoriesForAge(K08, null)[0]).toBe('implant');
    expect(recommendedCategoriesForAge(K08, undefined)[0]).toBe('implant');
  });

  test('只在「种植 + 义齿」并存时排序;单一类目不受年龄影响', () => {
    // 龋齿 90 岁也还是补牙,不该被改写
    expect(recommendedCategoriesForAge(['restorative'], 90)).toEqual(['restorative']);
    expect(recommendedCategoriesForAge(['endodontic'], 92)).toEqual(['endodontic']);
    expect(recommendedCategoriesForAge(['periodontic'], 88)).toEqual(['periodontic']);
  });

  test('⭐ 红线①:永不增删类目 —— 任何年龄下集合恒等', () => {
    for (const age of [null, 30, 70, 71, 90, 120]) {
      const r = recommendedCategoriesForAge(K08, age);
      expect([...r].sort()).toEqual([...K08].sort());
    }
  });

  test('空候选 → 空', () => {
    expect(recommendedCategoriesForAge([], 90)).toEqual([]);
  });
});

describe('treatmentCategoryNameZhFor — 展示标签', () => {
  test('⭐ >70 岁的 prosthodontic → 「活动义齿」(而非泛化"修复 / 冠桥")', () => {
    // 冠桥需邻牙条件,高龄同样受限;说清是活动义齿更贴合实际推荐
    expect(treatmentCategoryNameZhFor('prosthodontic', null, 90)).toBe('活动义齿');
  });

  test('未达线 → 原标签', () => {
    expect(treatmentCategoryNameZhFor('prosthodontic', null, 40)).toBe(
      treatmentCategoryNameZhForTeeth('prosthodontic', null),
    );
  });

  test('高龄的种植标签不变(仍叫"种植",只是排在义齿后)', () => {
    expect(treatmentCategoryNameZhFor('implant', null, 90)).toBe(
      treatmentCategoryNameZhForTeeth('implant', null),
    );
  });

  test('乳牙口径不被破坏(既有行为保留)', () => {
    expect(treatmentCategoryNameZhFor('restorative', '51;52', 8)).toBe('充填');
    expect(treatmentCategoryNameZhFor('restorative', '51;52', null)).toBe('充填');
  });
});

describe('组合:高龄缺牙的完整展示串', () => {
  test('⭐ 90 岁 → 「活动义齿 / 种植」(并行,义齿在前)', () => {
    const K08 = lookupDxTreatment('K08')!.categories;
    const s = recommendedCategoriesForAge(K08, 90)
      .map((c) => treatmentCategoryNameZhFor(c, null, 90))
      .join(' / ');
    expect(s).toBe('活动义齿 / 种植');
  });

  test('40 岁 → 原样(种植在前)', () => {
    const K08 = lookupDxTreatment('K08')!.categories;
    const s = recommendedCategoriesForAge(K08, 40)
      .map((c) => treatmentCategoryNameZhFor(c, null, 40))
      .join(' / ');
    expect(s.startsWith('种植')).toBe(true);
  });
});

describe('红线②:排除闸不受年龄影响', () => {
  test('K08 的 categories 仍是完整候选集(做过任一都算已解决)', () => {
    // 年龄适配只改"建议",绝不能收窄这个集合 ——
    // 否则高龄患者做过种植反而排除不掉(会重复召回)。
    const cats = lookupDxTreatment('K08')!.categories;
    expect(cats).toContain('implant');
    expect(cats).toContain('prosthodontic');
  });
});

// =============================================================
// 2026-07 线上反馈(陈芃霖 BJ0A094257):K00 细分 + 低龄种植闸 + 早矫措辞
// 三者与「活动义齿」同构 —— 只挪位 / 只改措辞,排除闸与是否召回一律不动。
// =============================================================

describe('refineCategoriesForDiagnosis — K00 同码内按诊断词定方向', () => {
  const K00 = lookupDxTreatment('K00')!.categories; // ['surgical','prosthodontic','implant','orthodontic']

  test('⭐ 乳牙早失 → 正畸在前(牙已经掉了,没得拔;临床是间隙保持/早矫)', () => {
    // 这就是线上那条错标:9 岁患儿 K00「乳牙早失」被打成「目标 · 外科」
    const r = refineCategoriesForDiagnosis('K00', '乳牙早失', K00);
    expect(r[0]).toBe('orthodontic');
  });

  test('乳牙滞留 / 多生牙 → 外科在前(有实体牙要拔,现状即正确)', () => {
    expect(refineCategoriesForDiagnosis('K00', '乳牙滞留', K00)[0]).toBe('surgical');
    expect(refineCategoriesForDiagnosis('K00', '多生牙', K00)[0]).toBe('surgical');
    expect(refineCategoriesForDiagnosis('K00', '滞留乳牙', K00)[0]).toBe('surgical');
  });

  test('先天缺失 / 釉质发育不全 → 修复在前', () => {
    expect(refineCategoriesForDiagnosis('K00', '先天缺失', K00)[0]).toBe('prosthodontic');
    expect(refineCategoriesForDiagnosis('K00', '先天缺牙', K00)[0]).toBe('prosthodontic');
    expect(refineCategoriesForDiagnosis('K00', '釉质发育不全', K00)[0]).toBe('prosthodontic');
  });

  test('异位萌出 / 迟萌 → 正畸在前(助萌导萌)', () => {
    expect(refineCategoriesForDiagnosis('K00', '异位萌出', K00)[0]).toBe('orthodontic');
    expect(refineCategoriesForDiagnosis('K00', '迟萌', K00)[0]).toBe('orthodontic');
  });

  test('⭐ 匹配顺序:「乳牙早失」不能被泛化的「缺失」规则吃掉', () => {
    expect(refineCategoriesForDiagnosis('K00', '乳牙早失', K00)[0]).toBe('orthodontic');
    expect(refineCategoriesForDiagnosis('K00', '先天缺失', K00)[0]).toBe('prosthodontic');
  });

  test('红线:只挪位,集合恒等(K00 四类目一个不少)', () => {
    for (const name of ['乳牙早失', '乳牙滞留', '先天缺失', '釉质发育不全', '异位萌出']) {
      const r = refineCategoriesForDiagnosis('K00', name, K00);
      expect([...r].sort()).toEqual([...K00].sort());
    }
  });

  test('红线:诊断码不动 —— 本函数不产出 code,K00 永远还是 K00', () => {
    // 与 canonical-codes 302 行那条 "后天缺牙 → K08" recode 是两件事:
    // 那条是宿主真标错了码;这里 K00 本身没标错,只是方向要细分。
    expect(lookupDxTreatment('K00')).toBeTruthy();
    expect(lookupDxTreatment('K00')!.categories).toEqual(K00);
  });

  test('回落:非 K00 / 空诊断词 / 未命中词 → 原样返回(旧数据无回归)', () => {
    const K08 = lookupDxTreatment('K08')!.categories;
    expect(refineCategoriesForDiagnosis('K08', '牙列缺损', K08)).toEqual(K08);
    expect(refineCategoriesForDiagnosis('K00', null, K00)).toEqual(K00);
    expect(refineCategoriesForDiagnosis('K00', '   ', K00)).toEqual(K00);
    expect(refineCategoriesForDiagnosis('K00', '混合牙列', K00)).toEqual(K00); // 未命中 → 原序
  });
});

describe('低龄种植闸 —— 与高龄义齿同构:只降位,不删', () => {
  const K08 = lookupDxTreatment('K08')!.categories;

  test('⭐ <18 岁缺牙 → 种植降到末位(颌骨未发育完成)', () => {
    const r = recommendedCategoriesForAge(K08, 9);
    expect(r[r.length - 1]).toBe('implant');
    expect(r[0]).not.toBe('implant');
  });

  test('红线:种植仍在集合里(不替医生下禁忌结论)', () => {
    const r = recommendedCategoriesForAge(K08, 9);
    expect([...r].sort()).toEqual([...K08].sort());
  });

  test('⭐ 边界:恰好 18 岁 → **降位**(与全仓另外三处种植年龄闸同口径 ≤18)', () => {
    // 差一岁就会自相矛盾 —— 2026-07-27 生产实测:恰好 18 岁有 1,299 条 plan、
    // 其中 140 条 focusCategory=implant;若这里按 <18 判,这 140 条会在同一屏上
    // 出现「目标 · 种植」+ 画像「种植禁忌」(contraindication.feature.ts 是 age <= 18)。
    expect(recommendedCategoriesForAge(K08, IMPLANT_LAST_AGE)[0]).not.toBe('implant');
    expect(recommendedCategoriesForAge(K08, IMPLANT_LAST_AGE - 1)[0]).not.toBe('implant');
    // 满 19 才恢复原序 —— 与 potential-treatment 的 `age > 18 才产潜在种植` 接得上
    expect(recommendedCategoriesForAge(K08, IMPLANT_LAST_AGE + 1)[0]).toBe('implant');
  });

  test('高龄 / 低龄两条互斥,各走各的', () => {
    expect(recommendedCategoriesForAge(K08, 90)[0]).toBe('prosthodontic'); // 高龄:义齿前置
    expect(recommendedCategoriesForAge(K08, 9)[0]).toBe('prosthodontic'); // 低龄:种植后置,同样是义齿/修复顶上
    expect(recommendedCategoriesForAge(K08, 35)[0]).toBe('implant'); // 中间年龄:原样
  });

  test('单类目不受影响(没得挪)', () => {
    expect(recommendedCategoriesForAge(['implant'], 9)).toEqual(['implant']);
  });
});

describe('早矫措辞 —— 与「活动义齿」同构:同类目,只改展示名', () => {
  test('⭐ ≤12 岁 orthodontic → 「早期矫治」', () => {
    expect(treatmentCategoryNameZhFor('orthodontic', null, 9)).toBe('早期矫治');
    expect(treatmentCategoryNameZhFor('orthodontic', null, EARLY_ORTHO_MAX_AGE)).toBe('早期矫治');
  });

  test('>12 岁 → 回落原名(恒牙列是常规正畸)', () => {
    expect(treatmentCategoryNameZhFor('orthodontic', null, EARLY_ORTHO_MAX_AGE + 1)).toBe(
      treatmentCategoryNameZhForTeeth('orthodontic', null),
    );
    expect(treatmentCategoryNameZhFor('orthodontic', null, 30)).toBe(
      treatmentCategoryNameZhForTeeth('orthodontic', null),
    );
  });

  test('无年龄 → 回落原名(不猜)', () => {
    expect(treatmentCategoryNameZhFor('orthodontic', null, null)).toBe(
      treatmentCategoryNameZhForTeeth('orthodontic', null),
    );
  });

  test('红线:只改措辞不改类目 —— K07 排除闸仍认 orthodontic', () => {
    expect(lookupDxTreatment('K07')!.categories).toEqual(['orthodontic']);
  });
});

describe('端到端:线上那条错标的修复(9 岁 · K00 乳牙早失 · 牙位 1C;1D;3D)', () => {
  test('⭐ 修前「目标 · 外科」→ 修后「目标 · 早期矫治」', () => {
    const K00 = lookupDxTreatment('K00')!.categories;
    const age = 9;
    // 后端 scenario 与前端 ReasonLine 走的是同一对函数、同一顺序
    const recCats = recommendedCategoriesForAge(
      refineCategoriesForDiagnosis('K00', '乳牙早失', K00),
      age,
    );
    expect(recCats[0]).toBe('orthodontic'); // focusCategory
    expect(treatmentCategoryNameZhFor(recCats[0]!, '1C;1D;3D', age)).toBe('早期矫治');
    // 未成年 → 种植沉到末位
    expect(recCats[recCats.length - 1]).toBe('implant');
    // 红线:集合没变 → 排除闸/召回与否完全不受影响
    expect([...recCats].sort()).toEqual([...K00].sort());
  });
});
