import {
  buildDraftRecallSummaryPrompt,
  DRAFT_RECALL_SUMMARY_SYSTEM,
  DRAFT_RECALL_SUMMARY_PROMPT_VERSION,
} from '../src/modules/ai/calls/draft-recall-summary/prompt';
import type { DraftRecallSummaryInput } from '../src/modules/ai/calls/draft-recall-summary/input.types';
import { DraftRecallSummaryCall } from '../src/modules/ai/calls/draft-recall-summary/call';

/**
 * 历史联系摘要 —— 「未来排程 ≠ 漏做」回归。
 *
 * 生产事故(2026-07-24 排查):详情页「历史联系」把**诊所已排好、还没到日子**的回访
 * 说成"最新一次未执行,需优先补做"。实例:
 *   · 张学军 task_date=2026-10-09(77 天后)→ "10月9日最新一次洁牙复查回访未执行"
 *   · 江世琪 2026-09-17(55 天后)、史原 2026-08-25(32 天后)—— 同款措辞
 * 当时 934 条已生成摘要里 196 条"最新回访是未来排程",其中 158 条(81%)带
 * "未执行/补做/漏/尚未"措辞;对照组(最新在过去)只有 38%。
 *
 * 根因:prompt **没有今日锚点**,且按 taskDate desc 排序 → 未来排程恒排第一被当成"最新一次";
 *      taskStatus(已预约/已完成)也没喂给模型。
 *
 * 修复纪律(本文件锁住):日期比较是**确定性计算**,由 orchestrator 算好 isFuture 传入,
 * prompt 只负责把它渲染成显式标记 —— 跟话术侧"能程序算的事实全算好、LLM 只润色"同一条规矩。
 */

const TODAY = '2026-07-24';

function item(over: Partial<DraftRecallSummaryInput['items'][0]> = {}) {
  return {
    taskDate: '2026-04-24',
    type: '常规回访',
    status: '已回访',
    taskStatus: '已完成',
    isFuture: false,
    isLatestPast: false,
    daysAgo: 91,
    treatmentItems: null,
    followContent: null,
    result: '治疗后回访',
    ...over,
  };
}

function build(items: DraftRecallSummaryInput['items']) {
  return buildDraftRecallSummaryPrompt({ patientNameMasked: '张*', today: TODAY, items });
}

describe('buildDraftRecallSummaryPrompt — 时间锚 + 未来排程标记', () => {
  test('⭐ 必须把"今天"写进 prompt(模型据此才能判断远近)', () => {
    expect(build([item()])).toContain(`今天:${TODAY}`);
  });

  test('⭐ 未来排程条目带【未来排程·尚未到期】标记', () => {
    const p = build([
      item({ taskDate: '2026-10-09', status: '未回访', taskStatus: '未完成', isFuture: true, followContent: '复查洁牙' }),
    ]);
    expect(p).toContain('【未来排程·尚未到期】');
    expect(p).toContain('2026-10-09');
  });

  test('已过期条目**不带**未来标记(否则真漏做会被洗白)', () => {
    const p = build([item({ taskDate: '2026-03-02', status: '未回访', taskStatus: '未完成' })]);
    expect(p).not.toContain('【未来排程·尚未到期】');
  });

  test('⭐ 有未来排程时追加显式警告,并报出条数', () => {
    const p = build([
      item({ taskDate: '2026-10-09', isFuture: true }),
      item({ taskDate: '2026-09-17', isFuture: true }),
      item(),
    ]);
    expect(p).toContain('其中 2 条');
    expect(p).toMatch(/不得表述为未执行/);
  });

  test('全是历史条目 → 不出现未来排程警告(不给无谓噪声)', () => {
    const p = build([item(), item({ taskDate: '2026-03-02' })]);
    expect(p).not.toContain('尚未到期的未来排程');
  });

  test('⭐ taskStatus 必须进 prompt ——「已预约」不是「没做」', () => {
    const p = build([item({ taskStatus: '已预约', result: '复诊邀约' })]);
    expect(p).toContain('任务:已预约');
  });

  test('三位真实患者的原始形态:最新一条是未来排程 → 都被正确标注', () => {
    // 生产实测数据(2026-07-24 当天)
    const real: Array<[string, string]> = [
      ['2026-10-09', '复查洁牙'], // 张学军
      ['2026-09-17', '复查洁牙  看其他治疗是否要做，没有保险了'], // 江世琪
      ['2026-08-25', '46复查拍CT'], // 史原
    ];
    for (const [date, content] of real) {
      const p = build([
        item({ taskDate: date, status: '未回访', taskStatus: '未完成', isFuture: true, followContent: content }),
      ]);
      expect(p).toContain('【未来排程·尚未到期】');
    }
  });
});

describe('DRAFT_RECALL_SUMMARY_SYSTEM — 硬约束文本', () => {
  test('⭐ system 明令未来排程不得说成未执行 / 漏做 / 需补做', () => {
    for (const word of ['未执行', '漏做', '需补做', '未来排程']) {
      expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain(word);
    }
  });

  test('⭐ 明令"最近一次/最新一次"只能指已过期条目', () => {
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toMatch(/最近一次|最新一次/);
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain('已过期');
  });

  test('明令不要自己比日期(确定性计算已由程序完成)', () => {
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain('不要自己比日期');
  });

  test('promptVersion 已 bump(改了 prompt 必须换版本,否则缓存/评估串档)', () => {
    expect(DRAFT_RECALL_SUMMARY_PROMPT_VERSION).not.toBe('draft_recall_summary@2026-06-16-d');
    expect(DRAFT_RECALL_SUMMARY_PROMPT_VERSION).not.toBe('draft_recall_summary@2026-07-24-e');
    expect(DRAFT_RECALL_SUMMARY_PROMPT_VERSION).toMatch(/^draft_recall_summary@\d{4}-\d{2}-\d{2}-[a-z]$/);
  });
});

/**
 * 「最近一次」突出 —— 2026-07 业务可用性测试回归。
 *
 * 原始反馈(患者余同学 BJ0L028292 详情页):
 *   [被测者]「说了常规的提醒没有特殊的」/[主持人]「那就是暂时没有办法详细的看到具体一次沟通什么内容」
 *
 * 生产核过的两件事,决定了这里怎么改:
 *   ① 源侧确实没有更多 —— DW 只有 fact_returnvisit_out 一张联系表,该患者 5 条全部
 *      落库无丢失;follow_content/return_visit_result 都是**标签**(「复查洁牙」「方案跟进」
 *      「无不适」「未接」),全库 165 万条平均 12.4 / 9.1 字。**没有**逐字沟通内容、
 *      患者原话、下次建议联系时间这三样东西。→ PAC 不造数据,摘要维持"只讲记录里写着的"。
 *   ② 但「最近一次」值得单独拎出来 —— 列表按日期倒序时头部常是未到期排程,
 *      客服最想知道的"上次聊到哪儿了"反而被埋在中间。
 *
 * 纪律不变:哪条是"最近一次"是**排序问题**,由 orchestrator 算好 isLatestPast 传入,
 * 不让 LLM 自己比日期(同 isFuture)。
 */
describe('最近一次·已发生 —— 程序标注 + 摘要开头必须讲它', () => {
  test('⭐ isLatestPast 条目带【最近一次·已发生】标记', () => {
    const p = build([item({ taskDate: '2026-05-20', isLatestPast: true, followContent: '复查洁牙' })]);
    expect(p).toContain('【最近一次·已发生】');
  });

  test('⭐ 未来排程即使排在最前也拿不到「最近一次」标记', () => {
    const p = build([
      item({ taskDate: '2026-10-09', isFuture: true, followContent: '复查洁牙' }),
      item({ taskDate: '2026-05-20', isLatestPast: true, followContent: '方案跟进' }),
    ]);
    const lines = p.split('\n');
    expect(lines.find((l) => l.includes('2026-10-09'))).toContain('【未来排程·尚未到期】');
    expect(lines.find((l) => l.includes('2026-10-09'))).not.toContain('【最近一次·已发生】');
    expect(lines.find((l) => l.includes('2026-05-20'))).toContain('【最近一次·已发生】');
  });

  test('⭐ 有最近一次 → 追加"放开头讲清楚"的指令', () => {
    const p = build([item({ isLatestPast: true })]);
    expect(p).toContain('放在摘要开头讲清楚');
  });

  test('⭐ 全是未来排程 → 明确告知"没有已发生的回访",不许硬凑最近一次', () => {
    const p = build([item({ taskDate: '2026-10-09', isFuture: true })]);
    expect(p).toContain('没有已发生的回访');
    expect(p).not.toContain('【最近一次·已发生】');
  });

  test('⭐ 最近一次没记结果 → 显式写「结果:未记录」(不写模型会拿别条的结果脑补)', () => {
    const p = build([
      item({ taskDate: '2026-05-20', status: '未回访', taskStatus: '未完成', isLatestPast: true, followContent: '复查洁牙', result: null }),
      item({ taskDate: '2026-02-23', result: '复查' }),
    ]);
    expect(p.split('\n').find((l) => l.includes('2026-05-20'))).toContain('结果:未记录');
  });

  test('非最近一次的空结果**不**补「未记录」(只在焦点那条上花笔墨)', () => {
    const p = build([item({ taskDate: '2026-02-23', result: null, followContent: '半年复查' })]);
    expect(p).not.toContain('结果:未记录');
  });

  test('⭐ 最近一次的正文预算更长 —— 源里几百字的方案沟通记录不能被 40 字砍没', () => {
    const long = '一'.repeat(150);
    expect(build([item({ isLatestPast: true, followContent: long })])).toContain('一'.repeat(150));
    expect(build([item({ followContent: long })])).not.toContain('一'.repeat(41));
  });

  test('⭐ system 明写"源系统没有逐字沟通内容",禁止补写患者原话', () => {
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain('没有逐字沟通内容');
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain('患者表示');
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toMatch(/先说「最近一次」/);
  });

  test('余同学 BJ0L028292 的生产原始形态 → 最近一次是 5-20 未回访、无结果', () => {
    // 生产实测(2026-07-26 查 DW fact_returnvisit_out,customer_id=1683102 brand=瑞尔,共 5 条)
    const p = buildDraftRecallSummaryPrompt({
      patientNameMasked: '余*',
      today: '2026-07-26',
      items: [
        item({ taskDate: '2026-05-20', status: '未回访', taskStatus: '未完成', isLatestPast: true, followContent: '复查洁牙', result: null }),
        item({ taskDate: '2026-02-25', status: '未回访', taskStatus: '未完成', followContent: '方案跟进', result: null }),
        item({ taskDate: '2026-02-23', status: '已回访', taskStatus: '已预约', followContent: null, result: '复查' }),
        item({ taskDate: '2025-11-16', status: '未回访', taskStatus: '未完成', followContent: '半年复查', result: null }),
        item({ taskDate: '2025-08-17', status: '已回访', taskStatus: '已完成', followContent: null, result: '复查' }),
      ],
    });
    expect(p).not.toContain('尚未到期的未来排程'); // 5 条全在过去
    const latest = p.split('\n').find((l) => l.includes('2026-05-20'))!;
    expect(latest).toContain('【最近一次·已发生】');
    expect(latest).toContain('内容:复查洁牙');
    expect(latest).toContain('结果:未记录');
  });
});

/**
 * 兜底文案(LLM 失败 / safety 拒收时展示给客服的那句)也走同一条时间纪律。
 * 原实现直接拿 items[0] 当「最近一次」—— 而 items 按日期倒序,头部很可能是未到期排程,
 * 等于把 2026-07 那次生产事故在兜底路径上原样复刻一遍。
 */
describe('DraftRecallSummaryCall.fallback — 兜底也不许把未来排程当最近一次', () => {
  const call = new DraftRecallSummaryCall();
  const run = (items: DraftRecallSummaryInput['items']) =>
    call.fallback({ patientNameMasked: '余*', today: TODAY, items }).summary;

  test('无记录 → 直说暂无', () => {
    expect(run([])).toContain('暂无历史联系记录');
  });

  test('⭐ 头部是未来排程时,「最近一次」取的是 isLatestPast 那条,不是 items[0]', () => {
    const s = run([
      item({ taskDate: '2026-10-09', isFuture: true, result: null, followContent: '复查洁牙' }),
      item({ taskDate: '2026-05-20', isLatestPast: true, result: '已约' }),
    ]);
    expect(s).toContain('2026-05-20');
    expect(s).not.toContain('2026-10-09');
  });

  test('⭐ 全是未来排程 → 不提「最近一次」', () => {
    const s = run([item({ taskDate: '2026-10-09', isFuture: true })]);
    expect(s).not.toContain('最近一次');
    expect(s).toContain('尚未到期');
  });

  test('最近一次没结果 → 兜底也如实说未留结果,不留空', () => {
    const s = run([item({ taskDate: '2026-05-20', isLatestPast: true, result: null, followContent: null })]);
    expect(s).toContain('未留结果');
  });
});

/**
 * ⭐ 职责边界 —— 「历史联系」只讲回访,不许对到诊/失联下结论。
 *
 * 生产事故(2026-07-27 排查):同一类错误换个轴又来一遍。上一次(07-24)是"未来 vs 过去",
 * 这次是"回访 vs 到诊"和"距今多久":
 *   · 陈芃霖:2023-08-15 回访结果原文「8月已就诊」→ 摘要写成"上次**有效到诊**为2023年8月",
 *     而 PAC 自己的接诊记录末次是 2025-12-20 —— 差 2 年 4 个月;
 *     同句还有"已超一年未复诊"(实为 7 个月)、"近3个月"(该回访实为 226 天前)。
 *   · 全量:795 条摘要里 162 条断言"失联/沉睡/未到诊",其中 88 条(54%)患者
 *     实际 180 天内来过;最离谱一条写"自2018年至今无到诊或有效联系,属长期失联状态",
 *     该患者上个月刚到诊。
 *
 * 根因:本 AiCall 的输入**只有回访记录,没有任何到诊数据**(见 input.types),
 *      模型只能从回访备注文字里猜、或用"没有回访"推"没来过"——两条路都错。
 *      而旧 prompt 不但没拦,示例里还有"此前半年 5 次回访无到诊"在示范这个错法。
 *
 * 本组用例锁两件事:① prompt 明确禁止到诊类结论;② 距今天数由程序算好喂进去。
 */
describe('⭐ 职责边界:只摘要回访事实,不判到诊', () => {
  test('system prompt 明写「没有到诊数据」并禁止相关结论', () => {
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain('没有任何到诊/就诊/复诊数据');
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain('禁止');
    // "没有回访 ≠ 没来过" 这条推理链必须被显式掐断
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain('"没有回访记录" ≠ "没来过"');
  });

  test('结果字段里的「已就诊」只能当那次回访的记录引述,不得当到诊状态', () => {
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain('不得**当成患者的到诊状态');
  });

  test('⭐ 示例里不能再出现示范错法的句子(旧版"5 次回访无到诊"就是元凶之一)', () => {
    const examples = DRAFT_RECALL_SUMMARY_SYSTEM.slice(DRAFT_RECALL_SUMMARY_SYSTEM.indexOf('# 示例'));
    expect(examples).not.toContain('回访无到诊');
    // 反例段必须把三条真实事故句列为 ✗,让模型见过错法长什么样
    expect(examples).toContain('上次有效到诊为2023年8月');
    expect(examples).toContain('长期失联状态');
  });

  test('关键要求里不再把「长期未到诊」列为要突出的信号', () => {
    const head = DRAFT_RECALL_SUMMARY_SYSTEM.slice(0, DRAFT_RECALL_SUMMARY_SYSTEM.indexOf('# ⭐ 职责边界'));
    expect(head).not.toContain('长期未到诊');
  });

  test('⭐ 距今天数由程序算好并渲染进 user prompt —— 模型不再自己减日期', () => {
    const s = build([item({ taskDate: '2025-12-10', isLatestPast: true, daysAgo: 226, result: '涂氟 邀约' })]);
    expect(s).toContain('226天前');
  });

  test('未来排程不报负数天数(已有【未来排程】标记,重复只会添乱)', () => {
    const s = build([item({ taskDate: '2026-10-09', isFuture: true, daysAgo: -77 })]);
    expect(s).not.toContain('-77');
    expect(s).not.toContain('77天前');
    expect(s).toContain('尚未到期');
  });

  test('无日期的条目不报天数', () => {
    const s = build([item({ taskDate: null, daysAgo: null })]);
    expect(s).not.toMatch(/\d+天前/);
  });

  test('时间纪律段要求按 daysAgo 换算相对时间', () => {
    expect(DRAFT_RECALL_SUMMARY_SYSTEM).toContain('距今多久也算好了');
  });
});
