import { describe, expect, it } from 'vitest';
import type { AssignmentProposal } from '@pac/types';
import {
  applyIncomingSheet,
  findActiveSheet,
  intentToPrompt,
  intentToSheetOps,
  toApiMessage,
  type Block,
  type ChatMessage,
  type DraftState,
  type ToolStep,
} from './chat-blocks';

/**
 * 草稿状态机的回归 —— 这段逻辑的失败形态**全都是静默的**：
 * 改错了卡片主管看不到反应、两张 active 同时能点确认就是分错一批人。
 * 端到端点一遍代价高且不稳定，所以抽成纯函数在这里锁住。
 */

const sheet = (n: number) => ({ placed: n } as unknown as AssignmentProposal);

function msg(id: string, ...sheets: Array<[string, DraftState]>): ChatMessage {
  return {
    id,
    role: 'assistant',
    blocks: sheets.map(([requestId, state]): Block => ({
      kind: 'assignment_sheet',
      requestId,
      sheet: sheet(1),
      state,
    })),
  };
}

/** 摊平成 [requestId, state] 便于断言 */
function states(ms: ChatMessage[]): Array<[string, DraftState]> {
  return ms.flatMap((m) =>
    m.blocks
      .filter((b): b is Extract<Block, { kind: 'assignment_sheet' }> => b.kind === 'assignment_sheet')
      .map((b) => [b.requestId, b.state] as [string, DraftState]),
  );
}

describe('applyIncomingSheet', () => {
  it('第一张草稿挂到宿主消息下，状态是 active', () => {
    const out = applyIncomingSheet([msg('m1')], {
      requestId: 'r1',
      sheet: sheet(10),
      hostMessageId: 'm1',
    });
    expect(states(out)).toEqual([['r1', 'active']]);
  });

  it('🔴 新草稿到达时，把**上一轮消息里**的 active 作废（跨消息扫描）', () => {
    const prev = [msg('m1', ['r1', 'active']), msg('m2')];
    const out = applyIncomingSheet(prev, {
      requestId: 'r2',
      sheet: sheet(20),
      hostMessageId: 'm2',
    });
    expect(states(out)).toEqual([
      ['r1', 'superseded'],
      ['r2', 'active'],
    ]);
  });

  it('🔴 终态不许被回退：confirmed / cancelled 不受作废扫描影响', () => {
    const prev = [msg('m1', ['r1', 'confirmed'], ['r2', 'cancelled']), msg('m2')];
    const out = applyIncomingSheet(prev, {
      requestId: 'r3',
      sheet: sheet(30),
      hostMessageId: 'm2',
    });
    expect(states(out)).toEqual([
      ['r1', 'confirmed'],
      ['r2', 'cancelled'],
      ['r3', 'active'],
    ]);
  });

  it('同一 requestId 重推：原地换内容、回到 active、⛔ 不堆第二张卡', () => {
    const withEdits: ChatMessage = {
      id: 'm1',
      role: 'assistant',
      blocks: [
        {
          kind: 'assignment_sheet',
          requestId: 'r1',
          sheet: sheet(1),
          state: 'active',
          edits: [{ seq: 1, ops: [] }],
        },
      ],
    };
    const out = applyIncomingSheet([withEdits], {
      requestId: 'r1',
      sheet: sheet(99),
      hostMessageId: 'm1',
    });
    expect(states(out)).toEqual([['r1', 'active']]);
    const b = out[0]!.blocks[0] as Extract<Block, { kind: 'assignment_sheet' }>;
    expect(b.sheet).toEqual(sheet(99));
    // ⚠️ 旧编辑指令必须清空 —— 对新名单没有意义
    expect(b.edits).toEqual([]);
  });

  it('任一时刻最多一张 active（连出三版）', () => {
    let ms: ChatMessage[] = [msg('m1')];
    for (const r of ['r1', 'r2', 'r3']) {
      ms = applyIncomingSheet(ms, { requestId: r, sheet: sheet(1), hostMessageId: 'm1' });
    }
    expect(states(ms).filter(([, s]) => s === 'active')).toHaveLength(1);
    expect(states(ms)).toEqual([
      ['r1', 'superseded'],
      ['r2', 'superseded'],
      ['r3', 'active'],
    ]);
  });
});

describe('findActiveSheet', () => {
  it('🔴 找的是 active，不是「最后一张」—— 这正是 2026-08-12 改掉的坑', () => {
    // 顺序上 r2 在后，但它已作废；助手的修改必须落到 r1 上
    const ms = [msg('m1', ['r1', 'active']), msg('m2', ['r2', 'superseded'])];
    const at = findActiveSheet(ms);
    expect(at).toEqual({ messageIndex: 0, blockIndex: 0 });
  });

  it('一张 active 都没有时返回 null（⛔ 不许退化成改最后一张）', () => {
    const ms = [msg('m1', ['r1', 'confirmed']), msg('m2', ['r2', 'superseded'])];
    expect(findActiveSheet(ms)).toBeNull();
  });

  it('没有确认单时返回 null', () => {
    expect(findActiveSheet([msg('m1')])).toBeNull();
  });
});

/**
 * intent 路由 —— 🔴 **按钮和助手必须走同一条路**。
 *
 * 这里锁的是"哪些 intent 走局部改单、哪些走重跑"，以及 owner/balance 不能弄反
 * （实测栽过：主管说「各自分给各自的专属客服」，助手用了铺平，18 人被散给 17 位别人）。
 */
describe('引导节点 intent 路由', () => {
  it('待分配三个动作 → 局部改单，且 owner / balance 不弄反', () => {
    expect(intentToSheetOps('pending.to_owner')).toEqual([
      { select: { group: 'pending' }, action: 'assign', to: { mode: 'owner' } },
    ]);
    expect(intentToSheetOps('pending.spread')).toEqual([
      { select: { group: 'pending' }, action: 'assign', to: { mode: 'balance' } },
    ]);
    expect(intentToSheetOps('pending.remove')).toEqual([
      { select: { group: 'pending' }, action: 'remove' },
    ]);
  });

  it('改时效 → 局部改单；缺 days → null（⛔ 不许瞎猜一个天数）', () => {
    expect(intentToSheetOps('expiry.set', { days: 5 })).toEqual([
      { select: { group: 'batch' }, action: 'set_expiry', days: 5 },
    ]);
    expect(intentToSheetOps('expiry.set')).toBeNull();
  });

  it('🔴 换人群 / 换基数 → ⛔ 不能局部改单，必须走重跑', () => {
    for (const i of ['cohort.narrow', 'cohort.widen', 'anchor.switch', 'batch_size.set', 'daily_rate.set']) {
      expect(intentToSheetOps(i)).toBeNull();
    }
    expect(intentToPrompt('cohort.widen')).toContain('重新排一版');
    expect(intentToPrompt('anchor.switch')).toContain('重新排一版');
    // 🔴 替主管说的那句话里**不许出现内部码**（这句会原样进对话框）
    const narrow = intentToPrompt('cohort.narrow', {
      personaTags: 'rfm:vip',
      labelZh: '重要价值',
      count: 62,
    })!;
    expect(narrow).toContain('重要价值');
    expect(narrow).toContain('62 人');
    expect(narrow).not.toContain('rfm:vip');
    // ⚠️ 没有中文名就宁可不发 —— ⛔ 不许退化成把码念出来
    expect(intentToPrompt('cohort.narrow', { personaTags: 'rfm:vip' })).toBeNull();
  });

  it('⛔ 两张表不能都认领同一个 intent（否则按钮行为取决于判断顺序）', () => {
    const all = [
      'pending.to_owner', 'pending.spread', 'pending.remove', 'pending.refill',
      'expiry.set', 'daily_rate.set', 'batch_size.set',
      'cohort.widen', 'cohort.narrow', 'anchor.switch',
    ];
    for (const i of all) {
      const ops = intentToSheetOps(i, { days: 5, personaTags: 'rfm:vip' });
      const text = intentToPrompt(i, { days: 5, personaTags: 'rfm:vip' });
      expect(ops === null || text === null).toBe(true);
    }
  });

  it('未知 intent → 两边都 null（卡片静默不动，⛔ 不许乱执行）', () => {
    expect(intentToSheetOps('nope.nope')).toBeNull();
    expect(intentToPrompt('nope.nope')).toBeNull();
  });
});

/**
 * 历史回传 —— 🔴 P3 的核心：**带全脉络，但分层带**。
 *
 * 两个失败方向都很贵：全量带 → 确认单几百个 uuid 灌进上下文且诱导复述；
 * 一点不带 → 模型每轮从零开始，只能拿自己上一轮说过的话当依据（背句子的温床）。
 */
describe('toApiMessage · 分层回传', () => {
  const tool = (over: Partial<ToolStep> = {}): Block => ({
    kind: 'tool',
    step: { id: 't', tool: 'get_cohort_attributes', args: { temperature: 'hot' }, status: 'done', result: '278 人', ...over },
  });

  it('🔴 工具调用会回传（此前整块丢弃 —— 模型每轮从零开始）', () => {
    const out = toApiMessage({ id: 'm', role: 'assistant', blocks: [tool()] })!;
    expect(out.content).toContain('get_cohort_attributes');
    expect(out.content).toContain('278 人');
  });

  it('🔴 按 blocks 原始顺序拼，⛔ 不把工具记录堆到末尾（先查什么再说什么本身是脉络）', () => {
    const out = toApiMessage({
      id: 'm',
      role: 'assistant',
      blocks: [{ kind: 'text', text: '我先看看' }, tool(), { kind: 'text', text: '这批 278 人' }],
    })!;
    expect(out.content.indexOf('我先看看')).toBeLessThan(out.content.indexOf('get_cohort'));
    expect(out.content.indexOf('get_cohort')).toBeLessThan(out.content.indexOf('这批 278 人'));
  });

  it('🔴 肥载荷会被截断（⛔ 不许把几百个 uuid 灌回上下文）', () => {
    const fat = JSON.stringify({ items: Array.from({ length: 400 }, (_, i) => `plan-uuid-${i}`) });
    const out = toApiMessage({ id: 'm', role: 'assistant', blocks: [tool({ result: fat })] })!;
    expect(out.content).toContain('（略）');
    expect(out.content.length).toBeLessThan(500);
  });

  it('⛔ 失败只留错因，不带原始堆栈（脏东西进了历史就一直在）', () => {
    const out = toApiMessage({
      id: 'm',
      role: 'assistant',
      blocks: [tool({ status: 'error', error: 'patient x 不在当前租户范围内\n  at Foo.bar (a.ts:1)\n  at Baz'.repeat(20) })],
    })!;
    expect(out.content).toContain('失败：');
    expect(out.content).not.toContain('at Foo.bar');
  });

  it('⭐ modelText 优先 —— 界面显示短的，模型拿到带完整批次 id 的那份', () => {
    const out = toApiMessage({
      id: 'm',
      role: 'assistant',
      blocks: [{ kind: 'text', text: '批次已确认', modelText: '批次已确认（id=abc-123）' }],
    })!;
    expect(out.content).toContain('abc-123');
  });

  it('⭐ 确认单只留一个状态锚，⛔ 不回传它的内容（事实在同轮的工具摘要里）', () => {
    const out = toApiMessage({
      id: 'm',
      role: 'assistant',
      blocks: [
        { kind: 'assignment_sheet', requestId: 'r1', sheet: sheet(1), state: 'superseded' },
        { kind: 'assignment_sheet', requestId: 'r2', sheet: sheet(2), state: 'active' },
      ],
    })!;
    expect(out.content).toContain('已作废');
    expect(out.content).toContain('当前有效');
  });

  it('⭐ 全空的消息返回 null（⛔ 别往上下文里塞空壳）', () => {
    expect(toApiMessage({ id: 'm', role: 'assistant', blocks: [] })).toBeNull();
  });
});
