import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { buildDraftRecallBriefPrompt } from '../src/modules/ai/calls/draft-recall-brief/prompt';
import type { DraftRecallBriefInput } from '../src/modules/ai/calls/draft-recall-brief/input.types';

/**
 * 🔴 简报第②句 —— 「**跟我们熟不熟** + 上次那件事」。
 *
 * ── 由来(2026-08-04 产品评审原话)────────────────────────────────
 *   「你应该把它严谨,明确写成三句话。第一句这个客户对我们的价值是什么?
 *     **第二句是他跟我们以前的关系亲密度是什么样的,就是联系他多不多,
 *     然后有没有过不好的体验**;第三是这一次要关怀他什么、让他来干什么。」
 * 原来第②句只写了"最近一次联系是什么事",**"多不多"和"不好的体验"整个缺失**。
 *
 * ── 「不好的体验」只有一种形态(2026-08-07 全量查证)───────────────
 * 产品定了这一句只看回访数据。实测这份数据里:
 *   · `type` 只有 常规回访 / 咨询回访 / 术后回访 —— 没有投诉类;
 *   · `result` 自由文本 3 万行 6765 种取值,长尾里 投诉/不满/纠纷/态度/退费 **一条都没有**;
 *   · 真正的负面只有**「挂断」**:688 次 / 595 位患者(占有回访记录患者的 11.5%)。
 * ⚠️ 第一遍扫的时候「无不适」被"不适"接住,假阳性 5816 条 —— 负面词表必须排除否定式。
 *
 * ⛔ 这里锁的三件事,做错了都不会报错:
 *   ① 总次数必须**单独喂**(回访历史只给最近 3 条,拿它数必然低估);
 *   ② 挂断为 0 时**整段不出现**(出现「被挂断 0 次」模型就会翻译成"沟通顺畅");
 *   ③ schema 描述与 prompt 渲染**必须同时存在** —— 只改一边 = 定义了但没喂。
 */

const BASE: DraftRecallBriefInput = {
  patientNameMasked: '王*',
  persona: [{ label: '价值分群', value: '重要发展客户' }],
  treatmentHistory: [{ category: '洁牙', count: 2 }],
  reasons: [
    {
      subLabel: '缺失牙未启动修复',
      source: 'diagnosis',
      diagnosis: '牙列缺损',
      tooth: '47',
      daysSinceText: '131 天(4 个月)',
      expectedCategories: ['种植'],
    },
  ],
  reviewAnchors: { lastVisitText: '8 个月前', lastCleaningText: null },
  doctorGuidance: [],
  returnVisitHistory: [
    {
      atText: '2 个月前',
      type: '常规回访',
      topicText: '半年洁牙提醒',
      resultText: '未接',
      reachedText: null,
    },
  ],
  contactStats: { total: 7, sinceText: '2 年' },
  hangUpCount: 0,
  benefitText: null,
};

describe('简报第②句 —— 跟我们熟不熟', () => {
  test('🔴 总联系次数必须单独喂 —— ⛔ 不能让模型去数那最近 3 条', () => {
    const p = buildDraftRecallBriefPrompt(BASE);
    expect(p).toContain('一共联系过 7 次');
    expect(p).toContain('最早是 2 年');
    // 明确告诉模型别拿下面那几条数总量(实测 1131 位患者被联系过 10 次以上)
    expect(p).toContain('别拿这几条数总次数');
  });

  test('🔴🔴 被挂断过 → 如实说;⛔ 没挂断过 → 整段不出现', () => {
    expect(buildDraftRecallBriefPrompt({ ...BASE, hangUpCount: 3 })).toContain('被挂断 3 次');
    // ⛔ 关键:0 的时候连"挂断"两个字都不许出现 ——
    //   一旦出现「被挂断 0 次」,模型就会顺手翻译成「沟通一直顺畅」,那是拿没证据当反面用
    const zero = buildDraftRecallBriefPrompt(BASE);
    expect(zero).not.toContain('被挂断');
    expect(zero).not.toContain('0 次');
  });

  test('⭐ 从没联系过 → 显式说"没联系过",⛔ 不留白(留白模型会脑补)', () => {
    const p = buildDraftRecallBriefPrompt({
      ...BASE,
      contactStats: { total: 0, sinceText: null },
      returnVisitHistory: [],
    });
    expect(p).toContain('此前从没联系过');
    expect(p).not.toContain('一共联系过');
  });

  test('⛔ 这一段的行首不许带字段名 —— 段名会被当措辞抄进输出', () => {
    // 前科:`,备注:xxx` 让模型输出「常规回访,备注半年洁牙提醒」;
    //      段名「本批福利」让它说出「本批有 8 折」。同一个病。
    const p = buildDraftRecallBriefPrompt({ ...BASE, hangUpCount: 2 });
    expect(p).not.toContain('contactStats');
    expect(p).not.toContain('hangUpCount');
  });

  test('🔴🔴 schema 描述与 prompt 渲染必须同时存在 —— 只改一边 = 定义了但没喂', () => {
    const SCHEMA = readFileSync(
      join(__dirname, '../src/modules/ai/calls/draft-recall-brief/schema.ts'),
      'utf8',
    );
    // schema 告诉模型"该说什么",prompt 决定"它看不看得到" —— 缺任一边都静默失效
    expect(SCHEMA).toMatch(/contactStats/);
    expect(SCHEMA).toMatch(/hangUpCount/);
    expect(SCHEMA).toMatch(/等于 0 时什么都不说/);
    const PROMPT = readFileSync(
      join(__dirname, '../src/modules/ai/calls/draft-recall-brief/prompt.ts'),
      'utf8',
    );
    expect(PROMPT).toMatch(/input\.contactStats\.total/);
    expect(PROMPT).toMatch(/input\.hangUpCount > 0/);
  });
});
