import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { focusOrderReasons, reasonMatchesLabel, subKeyRule } from '@pac/types';

/**
 * 聚焦排序 —— 「这一单客服该先谈哪件事」。
 *
 * 由来:实测本地库 40% 的在跑单跨多个业务标签,而详情页和话术都只认 `reasons[0]`。
 * 主管在矩阵点「补牙」分下来一批人,其中 **49%** 身上分最高的是别的病(缺牙/正畸),
 * 客服打开看到的聚焦项和整篇话术就是那个别的病 —— 主管的分配意图在最后一米丢了,
 * 而界面上没有任何迹象。
 *
 * 本文件锁四条:
 *   1. 命中批次标签的排第一(哪怕分更低)
 *   2. 无批次 / 撤销后 → 与老口径「分最高」**逐位相同**(回归保护)
 *   3. sub_key 带牙位后缀也要能匹配
 *   4. 三处排序都必须走同一个函数(纯文本扫描 —— 这个 bug 不会在类型或运行时暴露)
 */

const R = (subKey: string | null, priorityScore: number) => ({ subKey, priorityScore });

describe('focusOrderReasons — 批次意图优先', () => {
  test('⭐⭐ 命中批次标签的排第一,哪怕分更低', () => {
    const rows = [R('missing_tooth@15', 88), R('caries_no_filling@36', 55), R('perio_no_srp@whole', 70)];
    const out = focusOrderReasons(rows, 'filling');
    expect(out[0]!.subKey).toBe('caries_no_filling@36');
    // 其余仍按分排
    expect(out.slice(1).map((r) => r.priorityScore)).toEqual([88, 70]);
  });

  test('⭐⭐ 无批次标签 → 与老口径「分最高」逐位相同(回归保护)', () => {
    const rows = [R('caries_no_filling', 55), R('missing_tooth', 88), R('perio_no_srp', 70)];
    const legacy = [...rows].sort((a, b) => b.priorityScore - a.priorityScore);
    expect(focusOrderReasons(rows, null)).toEqual(legacy);
    expect(focusOrderReasons(rows, undefined)).toEqual(legacy);
  });

  test('⭐ 批次标签没有任何 reason 命中 → 同样回落分最高,不抛错', () => {
    const rows = [R('caries_no_filling', 55), R('missing_tooth', 88)];
    const out = focusOrderReasons(rows, 'early_ortho');
    expect(out.map((r) => r.priorityScore)).toEqual([88, 55]);
  });

  test('⭐ 不修改入参(调用方常把 Prisma 行直接传进来)', () => {
    const rows = [R('missing_tooth', 88), R('caries_no_filling', 55)];
    const snapshot = rows.map((r) => r.subKey);
    focusOrderReasons(rows, 'filling');
    expect(rows.map((r) => r.subKey)).toEqual(snapshot);
  });

  test('多条命中同一标签 → 它们之间仍按分排', () => {
    const rows = [R('caries_no_filling@36', 40), R('missing_tooth', 90), R('caries_no_filling@11', 60)];
    const out = focusOrderReasons(rows, 'filling');
    expect(out.map((r) => r.priorityScore)).toEqual([60, 40, 90]);
  });
});

describe('subKeyRule / reasonMatchesLabel', () => {
  test('⭐ sub_key 带牙位后缀也要能匹配(落库形态是 `规则@牙位`)', () => {
    expect(subKeyRule('caries_no_filling@36')).toBe('caries_no_filling');
    expect(subKeyRule('perio_no_srp@whole')).toBe('perio_no_srp');
    expect(subKeyRule('missing_tooth')).toBe('missing_tooth');
    expect(subKeyRule(null)).toBe('');
    expect(reasonMatchesLabel('caries_no_filling@36', 'filling')).toBe(true);
  });

  test('一对多的两条(年龄/诊断名才能定死)按候选集合判包含', () => {
    expect(reasonMatchesLabel('ortho_no_consult@whole', 'ortho')).toBe(true);
    expect(reasonMatchesLabel('ortho_no_consult@whole', 'early_ortho')).toBe(true);
    expect(reasonMatchesLabel('hard_tissue_damage@21', 'restoration')).toBe(true);
    expect(reasonMatchesLabel('hard_tissue_damage@21', 'extraction')).toBe(true);
  });

  test('⭐ 不属于 8 类业务标签的子规则永远不被选为聚焦项', () => {
    for (const rule of ['development_eruption', 'jaw_cyst', 'extraction_recommended']) {
      for (const lbl of ['implant', 'filling', 'extraction', 'ortho']) {
        expect(reasonMatchesLabel(rule, lbl)).toBe(false);
      }
    }
  });

  test('空标签 / 空 subKey 一律不匹配(⛔ 别让 undefined 意外命中)', () => {
    expect(reasonMatchesLabel('caries_no_filling', null)).toBe(false);
    expect(reasonMatchesLabel(null, 'filling')).toBe(false);
  });
});

/**
 * 🔴 三处排序必须走同一个函数。
 *
 * 这个 bug 的形态是:只改其中一处,另外两处**再排一次**把批次意图洗回"分最高" ——
 * 表现是"后端明明改了、界面没变化",不报错、类型也全绿,只能靠文本扫描锁住。
 */
describe('reasons 排序的三个消费方都走 focusOrderReasons', () => {
  const FILES = [
    ['详情页数据源', '../src/modules/plan-aggregate/plan-aggregate.service.ts'],
    ['话术编排器', '../src/modules/ai/orchestrators/plan-script.orchestrator.ts'],
  ] as const;

  test.each(FILES)('⭐⭐ %s 调用了 focusOrderReasons', (_name, rel) => {
    const src = readFileSync(join(__dirname, rel), 'utf-8');
    expect(src).toContain('focusOrderReasons(');
  });

  test('⭐⭐ 话术编排器不再直接按 priorityScore 手排 reasons', () => {
    const src = readFileSync(
      join(__dirname, '../src/modules/ai/orchestrators/plan-script.orchestrator.ts'),
      'utf-8',
    );
    expect(src).not.toMatch(/\[\.\.\.plan\.reasons\]\.sort/);
  });

  test('⭐⭐ 话术编排器加载的 reasons 数量足够(take≥12)—— 否则主管选的那条可能压根没被加载', () => {
    // take:3 时,若批次标签的 reason 排第 4,它不在候选里 → 聚焦排序无从提前,且**不报错**
    const src = readFileSync(
      join(__dirname, '../src/modules/ai/orchestrators/plan-script.orchestrator.ts'),
      'utf-8',
    );
    const m = src.match(/reasons:\s*\{\s*orderBy:\s*\{\s*priorityScore:\s*'desc'\s*\},\s*take:\s*(\d+)/);
    expect(m).not.toBeNull();
    expect(Number(m![1])).toBeGreaterThanOrEqual(12);
  });
});
