import { describe, expect, it } from 'vitest';
import type { AssignmentProposal } from '@pac/types';
import {
  applyIncomingSheet,
  findActiveSheet,
  intentToPrompt,
  intentToSheetOps,
  type Block,
  type ChatMessage,
  type DraftState,
} 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('重新排一版');
    expect(intentToPrompt('cohort.narrow', { personaTags: 'rfm:vip' })).toContain('rfm:vip');
  });

  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();
  });
});
