import { describe, expect, it, vi } from 'vitest';
import {
  authenticatedFetch,
  GongxueAiChatProvider,
  parseSsePayload,
  reduceAiSseMessage,
} from './provider';

describe('AI chat SSE message reducer', () => {
  it('separates reasoning and answer deltas', () => {
    let message = reduceAiSseMessage(undefined, {
      event: 'reasoning.delta',
      data: JSON.stringify({ messageId: 8, delta: '分析' }),
    });
    message = reduceAiSseMessage(message, {
      event: 'content.delta',
      data: JSON.stringify({ delta: '**答案**' }),
    });

    expect(message.reasoningContent).toBe('分析');
    expect(message.content).toBe('**答案**');
  });

  it('tracks tool lifecycle without exposing raw payloads', () => {
    let message = reduceAiSseMessage(undefined, {
      event: 'tool.started',
      data: JSON.stringify({
        toolCallId: 'call-1',
        toolName: 'search_students',
        summary: '姓名条件',
      }),
    });
    message = reduceAiSseMessage(message, {
      event: 'tool.completed',
      data: JSON.stringify({
        toolCallId: 'call-1',
        toolName: 'search_students',
        status: 'success',
        summary: '找到 1 条记录',
      }),
    });

    expect(message.toolRuns).toHaveLength(1);
    expect(message.toolRuns[0]).toMatchObject({ status: 'success', summary: '找到 1 条记录' });
  });

  it('tracks processed attachments and final feedback state', () => {
    let message = reduceAiSseMessage(undefined, {
      event: 'attachment.processed',
      data: JSON.stringify({
        attachment: {
          id: 4,
          name: '名单.xlsx',
          mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
          size: 1200,
          status: 'ready',
          url: '/api/ai/chat/attachments/4',
          createdAt: '2026-07-24T00:00:00.000Z',
        },
      }),
    });
    message = reduceAiSseMessage(message, {
      event: 'message.completed',
      data: JSON.stringify({
        message: {
          id: 12,
          content: '完成',
          reasoningContent: null,
          feedback: 'like',
          attachments: message.attachments,
        },
      }),
    });
    expect(message.attachments).toHaveLength(1);
    expect(message.feedback).toBe('like');
  });

  it('uses final content and records cancellation and errors', () => {
    let message = reduceAiSseMessage(undefined, {
      event: 'message.completed',
      data: JSON.stringify({
        message: {
          id: 12,
          content: '最终回答',
          reasoningContent: '完成',
          status: 'completed',
          toolRuns: [
            {
              toolCallId: 'nested-tool',
              toolName: 'search_rooms',
              status: 'success',
              resultSummary: '共 4 间',
            },
          ],
        },
      }),
    });
    message = reduceAiSseMessage(message, {
      event: 'error',
      data: JSON.stringify({ message: '上游服务不可用' }),
    });
    message = reduceAiSseMessage(message, {
      event: 'message.cancelled',
      data: JSON.stringify({ messageId: 12 }),
    });

    expect(message).toMatchObject({
      id: 12,
      content: '最终回答',
      reasoningContent: '完成',
      error: '上游服务不可用',
      cancelled: true,
    });
    expect(message.toolRuns[0]).toMatchObject({ toolCallId: 'nested-tool', status: 'success' });
  });

  it('reads nested assistant message from message.created', () => {
    const message = reduceAiSseMessage(undefined, {
      event: 'message.created',
      data: JSON.stringify({
        message: { id: 9, content: '', reasoningContent: null, status: 'pending' },
      }),
    });

    expect(message.id).toBe(9);
  });

  it('merges ui.form events into the assistant message by id', () => {
    const form = {
      id: 'form-1',
      title: '新增学生',
      submitLabel: '提交创建',
      fields: [
        { name: 'name', label: '姓名', type: 'input', required: true },
        { name: 'gender', label: '性别', type: 'select', options: [{ label: '男', value: 'male' }] },
      ],
    };
    let message = reduceAiSseMessage(undefined, {
      event: 'ui.form',
      data: JSON.stringify({ messageId: 8, form }),
    });
    message = reduceAiSseMessage(message, {
      event: 'ui.form',
      data: JSON.stringify({ messageId: 8, form: { ...form, id: 'form-1' } }),
    });
    message = reduceAiSseMessage(message, {
      event: 'ui.form',
      data: JSON.stringify({
        messageId: 8,
        form: { id: 'form-2', title: '入住确认', fields: [] },
      }),
    });

    expect(message.forms).toHaveLength(2);
    expect(message.forms?.[0]).toMatchObject({ id: 'form-1', title: '新增学生' });
    expect(message.forms?.[1]).toMatchObject({ id: 'form-2' });
  });

  it('restores a persisted form from message.completed metadata', () => {
    const message = reduceAiSseMessage(undefined, {
      event: 'message.completed',
      data: JSON.stringify({
        message: {
          id: 12,
          content: '请填写表单',
          status: 'completed',
          metadata: {
            a2uiForm: {
              id: 'form-9',
              title: '新增学生',
              fields: [{ name: 'name', label: '姓名', type: 'input', required: true }],
            },
          },
        },
      }),
    });

    expect(message.forms).toHaveLength(1);
    expect(message.forms?.[0].id).toBe('form-9');
  });

  it('merges ui.review events into the assistant message and updates by id', () => {
    const review = {
      id: 'review-1',
      title: '开学导入',
      summary: '来自报名 Excel',
      status: 'pending',
      sections: [
        {
          key: 'students',
          type: 'students',
          title: '学生',
          kind: 'table',
          columns: [
            { key: 'name', title: '姓名' },
            { key: 'phone', title: '手机号' },
          ],
          rows: [{ name: '张三', phone: '13800138000' }],
          issues: [],
        },
      ],
    };
    let message = reduceAiSseMessage(undefined, {
      event: 'ui.review',
      data: JSON.stringify({ messageId: 8, review }),
    });
    message = reduceAiSseMessage(message, {
      event: 'ui.review',
      data: JSON.stringify({
        messageId: 8,
        review: { ...review, status: 'submitted', resultSummary: '{"students":{"created":1}}' },
      }),
    });

    expect(message.reviews).toHaveLength(1);
    expect(message.reviews?.[0]).toMatchObject({ id: 'review-1', status: 'submitted' });
  });

  it('shows model retrying state and clears it when content starts', () => {
    let message = reduceAiSseMessage(undefined, {
      event: 'model.retrying',
      data: JSON.stringify({
        messageId: 8,
        retry: { attempt: 1, maxRetries: 3, delayMs: 500, reason: '上游返回 503' },
      }),
    });
    expect(message.retrying).toMatchObject({ attempt: 1, maxRetries: 3 });
    message = reduceAiSseMessage(message, {
      event: 'content.delta',
      data: JSON.stringify({ messageId: 8, delta: '你好' }),
    });
    expect(message.retrying).toBeNull();
    expect(message.content).toContain('你好');
  });

  it('restores a persisted review from message.completed metadata', () => {
    const message = reduceAiSseMessage(undefined, {
      event: 'message.completed',
      data: JSON.stringify({
        message: {
          id: 12,
          content: '请审阅',
          status: 'completed',
          metadata: {
            a2uiReview: {
              id: 'review-9',
              title: '批量导入',
              status: 'pending',
              sections: [
                {
                  key: 'rooms',
                  type: 'rooms',
                  title: '宿舍',
                  kind: 'table',
                  columns: [{ key: 'roomNumber', title: '房间号' }],
                  rows: [{ roomNumber: '3-301' }],
                  issues: [],
                },
              ],
            },
          },
        },
      }),
    });

    expect(message.reviews).toHaveLength(1);
    expect(message.reviews?.[0]).toMatchObject({ id: 'review-9', title: '批量导入' });
  });

  it('merges ui.chart events into the assistant message by id', () => {
    const chart = {
      id: 'chart-1',
      title: '各班级人数',
      chartType: 'bar',
      columns: [
        { key: 'className', title: '班级' },
        { key: 'count', title: '人数' },
      ],
      rows: [
        { className: '一班', count: 20 },
        { className: '二班', count: 15 },
      ],
    };
    let message = reduceAiSseMessage(undefined, {
      event: 'ui.chart',
      data: JSON.stringify({ messageId: 8, chart }),
    });
    message = reduceAiSseMessage(message, {
      event: 'ui.chart',
      data: JSON.stringify({
        messageId: 8,
        chart: { ...chart, id: 'chart-2', title: '女生人数' },
      }),
    });

    expect(message.charts).toHaveLength(2);
    expect(message.charts?.[0]).toMatchObject({ id: 'chart-1', chartType: 'bar' });
    expect(message.charts?.[1]).toMatchObject({ id: 'chart-2' });
  });

  it('restores persisted charts from message.completed metadata', () => {
    const message = reduceAiSseMessage(undefined, {
      event: 'message.completed',
      data: JSON.stringify({
        message: {
          id: 12,
          content: '这是图表',
          status: 'completed',
          metadata: {
            a2uiChart: [
              {
                id: 'chart-9',
                title: '男女比例',
                chartType: 'pie',
                columns: [
                  { key: 'name', title: '性别' },
                  { key: 'value', title: '人数' },
                ],
                rows: [
                  { name: '男', value: 20 },
                  { name: '女', value: 15 },
                ],
              },
            ],
          },
        },
      }),
    });

    expect(message.charts).toHaveLength(1);
    expect(message.charts?.[0]).toMatchObject({ id: 'chart-9', chartType: 'pie' });
  });

  it('rewrites review submissions to the review submit stream endpoint', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
    vi.stubGlobal('fetch', fetchMock);
    try {
      await authenticatedFetch('http://x/api/ai/chat/conversations/3/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          message: '确认批量导入',
          attachmentIds: [],
          skillKey: null,
          clientRequestId: '6a8bc680-3cb5-4f2d-85ee-974974e0f194',
          reasoningEffort: 'high',
          reviewSubmission: { reviewId: 'review-1', reviewTitle: '开学导入' },
        }),
      });
      expect(String(fetchMock.mock.calls[0][0])).toBe(
        'http://x/api/ai/chat/reviews/review-1/submit/stream',
      );
      const body = JSON.parse(
        (fetchMock.mock.calls[0][1] as RequestInit).body as string,
      ) as Record<string, unknown>;
      expect(body).toEqual({
        clientRequestId: '6a8bc680-3cb5-4f2d-85ee-974974e0f194',
        reasoningEffort: 'high',
      });
    } finally {
      vi.unstubAllGlobals();
    }
  });

  it('keeps reasoningEffort when rewriting regenerate requests', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
    vi.stubGlobal('fetch', fetchMock);
    try {
      await authenticatedFetch('http://x/api/ai/chat/conversations/3/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          message: '',
          attachmentIds: [],
          skillKey: null,
          clientRequestId: '6a8bc680-3cb5-4f2d-85ee-974974e0f194',
          reasoningEffort: 'high',
          regenerateMessageId: 99,
        }),
      });
      expect(String(fetchMock.mock.calls[0][0])).toBe(
        'http://x/api/ai/chat/conversations/3/messages/99/regenerate/stream',
      );
      const body = JSON.parse(
        (fetchMock.mock.calls[0][1] as RequestInit).body as string,
      ) as Record<string, unknown>;
      expect(body).toEqual({
        clientRequestId: '6a8bc680-3cb5-4f2d-85ee-974974e0f194',
        reasoningEffort: 'high',
      });
    } finally {
      vi.unstubAllGlobals();
    }
  });

  it('keeps reasoningEffort when rewriting form submissions', async () => {
    const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
    vi.stubGlobal('fetch', fetchMock);
    try {
      await authenticatedFetch('http://x/api/ai/chat/conversations/3/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          message: '',
          attachmentIds: [],
          skillKey: null,
          clientRequestId: '6a8bc680-3cb5-4f2d-85ee-974974e0f194',
          reasoningEffort: 'high',
          formSubmission: { formId: 'form-1', values: { name: '张三' } },
        }),
      });
      expect(String(fetchMock.mock.calls[0][0])).toBe(
        'http://x/api/ai/chat/forms/form-1/submit/stream',
      );
      const body = JSON.parse(
        (fetchMock.mock.calls[0][1] as RequestInit).body as string,
      ) as Record<string, unknown>;
      expect(body).toEqual({
        clientRequestId: '6a8bc680-3cb5-4f2d-85ee-974974e0f194',
        values: { name: '张三' },
        reasoningEffort: 'high',
      });
    } finally {
      vi.unstubAllGlobals();
    }
  });

  it('routes submit-time ui.review to the original message instead of the streaming one', () => {
    const provider = new GongxueAiChatProvider('http://x/api/ai/chat/conversations/3/stream');
    const onExternalReview = vi.fn();
    provider.onExternalReview = onExternalReview;
    const review = {
      id: 'review-1',
      title: '批量导入',
      status: 'submitted',
      sections: [],
    };
    const origin = {
      id: 13,
      role: 'assistant' as const,
      content: '生成中',
      reasoningContent: '',
      toolRuns: [],
      attachments: [],
      reviews: [],
    };
    const next = provider.transformMessage({
      originMessage: origin,
      chunk: { event: 'ui.review', data: JSON.stringify({ messageId: 12, review }) },
      status: 'updating',
      chunks: [],
      responseHeaders: {} as Headers,
    });

    expect(onExternalReview).toHaveBeenCalledWith(12, review);
    expect(next).toBe(origin);
    expect(next.reviews ?? []).toHaveLength(0);
  });

  it('routes ui.review without an origin message to the external handler', () => {
    const provider = new GongxueAiChatProvider('http://x/api/ai/chat/conversations/3/stream');
    const onExternalReview = vi.fn();
    provider.onExternalReview = onExternalReview;
    const next = provider.transformMessage({
      chunk: {
        event: 'ui.review',
        data: JSON.stringify({
          messageId: 12,
          review: { id: 'review-1', title: '批量导入', status: 'submitted', sections: [] },
        }),
      },
      status: 'updating',
      chunks: [],
      responseHeaders: {} as Headers,
    });

    expect(onExternalReview).toHaveBeenCalledWith(
      12,
      expect.objectContaining({ id: 'review-1' }),
    );
    expect(next.reviews ?? []).toHaveLength(0);
  });

  it('tolerates non-JSON event data', () => {
    expect(parseSsePayload({ event: 'content.delta', data: 'plain text' })).toEqual({
      event: 'content.delta',
      payload: { delta: 'plain text' },
    });
  });
});
