import type {ChatTimelineItem} from '../types';

import {describe, expect, it} from 'vitest';

import {ChatTimelineBuilder} from '../builder';

describe('chat timeline builder', () => {
  it('keeps streamed thinking, text, tool and following text in timeline order', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendThinkingDelta(items, '先思考');
    ChatTimelineBuilder.appendMessageDelta(items, '正文 A');
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-1',
      toolName: '查询工具',
      input: { keyword: 'EasyFlow' },
    });
    ChatTimelineBuilder.appendMessageDelta(items, '正文 B');

    expect(items).toHaveLength(3);
    expect(items[0]?.type).toBe('message');
    expect(items[1]?.type).toBe('tool');
    expect(items[2]?.type).toBe('message');

    const firstMessage = items[0];
    expect(firstMessage?.type).toBe('message');
    if (firstMessage?.type === 'message') {
      expect(firstMessage.parts.map((part) => part.type)).toEqual([
        'thinking',
        'text',
      ]);
      expect(firstMessage.parts[0]?.content).toBe('先思考');
      expect(firstMessage.parts[1]?.content).toBe('正文 A');
    }

    const secondMessage = items[2];
    expect(secondMessage?.type).toBe('message');
    if (secondMessage?.type === 'message') {
      expect(secondMessage.parts.map((part) => part.type)).toEqual(['text']);
      expect(secondMessage.parts[0]?.content).toBe('正文 B');
    }
  });

  it('keeps round metadata on user and assistant messages', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendUserMessage(items, '问题', {
      roundId: 'round-1',
    });
    ChatTimelineBuilder.appendMessageDelta(items, '回答', {
      roundId: 'round-1',
      variantIndex: 1,
    });
    ChatTimelineBuilder.finalize(items);

    expect(items).toHaveLength(2);
    expect(items[0]?.type).toBe('message');
    expect(items[1]?.type).toBe('message');
    if (items[0]?.type === 'message' && items[1]?.type === 'message') {
      expect(items[0].roundId).toBe('round-1');
      expect(items[1].roundId).toBe('round-1');
      expect(items[1].variantIndex).toBe(1);
      expect(items[1].status).toBe('done');
    }
  });

  it('updates tool result by toolCallId instead of adding another card', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-1',
      toolName: '查询工具',
      input: { keyword: 'EasyFlow' },
    });
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-1',
      toolName: '查询工具',
      output: { result: 'ok' },
      status: 'success',
    });

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('tool');
    if (items[0]?.type === 'tool') {
      expect(items[0].status).toBe('success');
      expect(items[0].input).toEqual({ keyword: 'EasyFlow' });
      expect(items[0].output).toEqual({ result: 'ok' });
    }
  });

  it('shows built-in knowledge retrieval as a lightweight status row', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-knowledge',
      toolName: 'retrieve_knowledge',
      input: { query: '请假安排' },
    });

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('status');
    if (items[0]?.type === 'status') {
      expect(items[0].label).toBe('正在检索知识库');
      expect(items[0].status).toBe('running');
    }

    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-knowledge',
      toolName: 'retrieve_knowledge',
      output: { result: 'ok' },
      status: 'success',
    });

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('status');
    if (items[0]?.type === 'status') {
      expect(items[0].label).toBe('已检索知识库');
      expect(items[0].status).toBe('done');
    }
  });

  it('shows memory compression status as a lightweight status row', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
      label: '正在整理上下文',
      phase: 'started',
      status: 'running',
      statusKey: 'memory-compression',
    });

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('status');
    if (items[0]?.type === 'status') {
      expect(items[0].label).toBe('正在整理上下文');
      expect(items[0].presentation).toBe('separator');
      expect(items[0].status).toBe('running');
      expect(items[0].statusKey).toBe('memory-compression');
    }

    ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
      label: '已整理上下文',
      phase: 'completed',
      status: 'done',
      statusKey: 'memory-compression',
      compressed: true,
    });

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('status');
    if (items[0]?.type === 'status') {
      expect(items[0].label).toBe('已整理上下文');
      expect(items[0].presentation).toBe('separator');
      expect(items[0].status).toBe('done');
      expect(items[0].statusKey).toBe('memory-compression');
    }
  });

  it('removes memory compression status when compression produced no compressed event', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
      label: '正在整理上下文',
      phase: 'started',
      status: 'running',
      statusKey: 'memory-compression',
    });
    ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
      compressed: false,
      label: '无需压缩上下文',
      phase: 'completed',
      status: 'done',
      statusKey: 'memory-compression',
    });

    expect(items).toHaveLength(0);
  });

  it('ends current thinking before showing knowledge retrieval status', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendThinkingDelta(items, '需要先检索知识库');
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-knowledge',
      toolName: 'retrieve_knowledge',
    });
    ChatTimelineBuilder.appendThinkingDelta(items, '开始分析检索结果');

    expect(items).toHaveLength(3);
    expect(items[0]?.type).toBe('message');
    expect(items[1]?.type).toBe('status');
    expect(items[2]?.type).toBe('message');
    if (items[0]?.type === 'message' && items[2]?.type === 'message') {
      expect(items[0].status).toBe('done');
      expect(items[0].roundCompleted).not.toBe(true);
      expect(items[0].parts[0]).toMatchObject({
        content: '需要先检索知识库',
        expanded: false,
        status: 'end',
        type: 'thinking',
      });
      expect(items[2].parts[0]).toMatchObject({
        content: '开始分析检索结果',
        expanded: true,
        status: 'thinking',
        type: 'thinking',
      });
    }
  });

  it('keeps AgentScope internal fragment hidden without showing knowledge status', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-fragment',
      toolName: '__fragment__',
      input: { query: '暑假安排' },
    });
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-fragment',
      toolName: '__fragment__',
      output: { result: 'ok' },
      status: 'success',
    });

    expect(items).toHaveLength(0);
  });

  it('keeps AgentScope context reload hidden without showing a tool card', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-context-reload',
      toolName: 'context_reload',
      input: { working_context_offload_uuid: 'context-id' },
    });
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-context-reload',
      toolName: 'context_reload',
      output: { result: 'ok' },
      status: 'success',
    });

    expect(items).toHaveLength(0);
  });

  it('ignores anonymous tool call events instead of rendering a fallback card', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-fragment',
      input: { arguments: '{"query":"test"}' },
    });

    expect(items).toHaveLength(0);
  });

  it('does not let hidden tool events overwrite a visible tool card', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-1',
      toolName: '查询工具',
      input: { keyword: 'EasyFlow' },
    });
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-1',
      toolName: '__fragment__',
      input: { arguments: '{"keyword":"EasyFlow"}' },
    });

    expect(items).toHaveLength(1);
    expect(items[0]).toMatchObject({
      toolCallId: 'call-1',
      toolName: '查询工具',
      type: 'tool',
    });
  });

  it('keeps knowledge retrieval events in one timeline status row', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-knowledge-1',
      toolName: 'retrieve_knowledge',
    });
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-knowledge-2',
      toolName: 'retrieve_knowledge',
    });
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-knowledge-1',
      toolName: 'retrieve_knowledge',
      status: 'success',
    });
    ChatTimelineBuilder.upsertKnowledgeRetrievalStatus(
      items,
      'done',
      'knowledge-retrieval',
    );

    expect(items).toHaveLength(1);
    expect(items.every((item) => item.type === 'status')).toBe(true);
    if (items[0]?.type === 'status') {
      expect(items[0].label).toBe('已检索知识库');
      expect(items[0].status).toBe('done');
      expect(items[0].statusKey).toBe('knowledge-retrieval');
    }
  });

  it('attaches final knowledge citations to assistant message', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendMessageDelta(items, '暑假安排如下');
    ChatTimelineBuilder.appendKnowledge(items, [
      {
        chunkContent: '暑假安排原文',
        chunkId: 'faq-1',
        faqCollection: true,
        knowledgeName: '学生事务 FAQ',
      },
    ]);

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('message');
    if (items[0]?.type === 'message') {
      expect(items[0].knowledgeItems?.[0]?.chunkContent).toBe('暑假安排原文');
      expect(items[0].knowledgeItems?.[0]?.knowledgeName).toBe('学生事务 FAQ');
    }
  });

  it('keeps knowledge citations as fallback item without assistant message', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendKnowledge(items, [
      {
        chunkContent: '第一段',
        chunkId: 'chunk-1',
        documentId: 'doc-1',
        documentName: '治理方案.docx',
      },
    ]);

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('knowledge');
    if (items[0]?.type === 'knowledge') {
      expect(items[0].items).toHaveLength(1);
    }
  });

  it('keeps approval tool lifecycle in one card', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendToolApproval(items, {
      requestId: 'request-1',
      resumeToken: 'resume-1',
      toolCallId: 'call-1',
      toolName: '审批工具',
      input: { keyword: 'EasyFlow' },
    });
    ChatTimelineBuilder.markToolApproving(items, {
      requestId: 'request-1',
      resumeToken: 'resume-1',
      toolCallId: 'call-1',
    });
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-1',
      toolName: '审批工具',
      status: 'running',
    });
    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-1',
      output: { result: 'ok' },
      status: 'success',
    });

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('tool');
    if (items[0]?.type === 'tool') {
      expect(items[0].mode).toBe('approval');
      expect(items[0].status).toBe('success');
      expect(items[0].approval?.requestId).toBe('request-1');
      expect(items[0].input).toEqual({ keyword: 'EasyFlow' });
      expect(items[0].output).toEqual({ result: 'ok' });
    }
  });

  it('marks approval tool rejected in the same card', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendToolApproval(items, {
      requestId: 'request-1',
      resumeToken: 'resume-1',
      toolCallId: 'call-1',
      toolName: '审批工具',
      input: { keyword: 'EasyFlow' },
    });
    ChatTimelineBuilder.markToolRejected(items, {
      requestId: 'request-1',
      toolCallId: 'call-1',
      reason: '用户拒绝执行',
    });

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('tool');
    if (items[0]?.type === 'tool') {
      expect(items[0].mode).toBe('approval');
      expect(items[0].status).toBe('rejected');
      expect(items[0].rejectReason).toBe('用户拒绝执行');
    }
  });

  it('keeps approval and auto updates separate when toolCallId differs', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.upsertToolCall(items, {
      toolCallId: 'call-1',
      toolName: '查询工具',
      input: { keyword: 'before approval' },
    });
    ChatTimelineBuilder.appendToolApproval(items, {
      requestId: 'request-1',
      resumeToken: 'resume-1',
      toolCallId: 'call-2',
      toolName: '查询工具',
      input: { keyword: 'approval' },
    });

    expect(items).toHaveLength(2);
    expect(items[0]?.type).toBe('tool');
    expect(items[1]?.type).toBe('tool');
    if (items[0]?.type === 'tool' && items[1]?.type === 'tool') {
      expect(items[0].toolCallId).toBe('call-1');
      expect(items[0].mode).toBe('auto');
      expect(items[1].toolCallId).toBe('call-2');
      expect(items[1].mode).toBe('approval');
      expect(items[1].status).toBe('pending_approval');
      expect(items[1].approval?.requestId).toBe('request-1');
      expect(items[1].input).toEqual({ keyword: 'approval' });
    }
  });

  it('streams thinking content in a thinking part', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendThinkingDelta(items, 'A');
    ChatTimelineBuilder.appendThinkingDelta(items, 'B');

    expect(items).toHaveLength(1);
    expect(items[0]?.type).toBe('message');
    if (items[0]?.type === 'message') {
      expect(items[0].parts).toHaveLength(1);
      expect(items[0].parts[0]).toMatchObject({
        content: 'AB',
        expanded: true,
        status: 'thinking',
        type: 'thinking',
      });
    }
  });

  it('keeps reasoning content separate from normal text payloads', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendThinkingDelta(items, '思考中');
    ChatTimelineBuilder.appendMessageDelta(items, '正文');

    expect(items).toHaveLength(1);
    if (items[0]?.type === 'message') {
      expect(items[0].parts.map((part) => part.type)).toEqual([
        'thinking',
        'text',
      ]);
      expect(items[0].parts[0]?.content).toBe('思考中');
      expect(items[0].parts[0]).toMatchObject({
        expanded: false,
        status: 'end',
      });
      expect(items[0].parts[1]?.content).toBe('正文');
    }
  });

  it('appends duplicated text deltas without filtering model output', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendThinkingDelta(items, '思考中');
    ChatTimelineBuilder.appendMessageDelta(items, '你好啊');
    ChatTimelineBuilder.appendMessageDelta(items, '你好啊');

    expect(items).toHaveLength(1);
    if (items[0]?.type === 'message') {
      expect(items[0].parts.map((part) => part.type)).toEqual([
        'thinking',
        'text',
      ]);
      expect(items[0].parts[1]?.content).toBe('你好啊你好啊');
    }
  });

  it('appends accumulated-looking text without guessing protocol semantics', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendThinkingDelta(items, '思考中');
    ChatTimelineBuilder.appendMessageDelta(items, '你好啊！很高兴见到');
    ChatTimelineBuilder.appendMessageDelta(
      items,
      '你好啊！很高兴见到你！有什么我可以帮助你的吗？',
    );

    expect(items).toHaveLength(1);
    if (items[0]?.type === 'message') {
      expect(items[0].parts.map((part) => part.type)).toEqual([
        'thinking',
        'text',
      ]);
      expect(items[0].parts[1]?.content).toBe(
        '你好啊！很高兴见到你好啊！很高兴见到你！有什么我可以帮助你的吗？',
      );
    }
  });

  it('keeps markdown and code text exactly as streamed', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendThinkingDelta(items, '思考中');
    ChatTimelineBuilder.appendMessageDelta(items, '## 标题\n');
    ChatTimelineBuilder.appendMessageDelta(items, '| 模型 | 说明 |\n');
    ChatTimelineBuilder.appendMessageDelta(items, '| --- | --- |\n');
    ChatTimelineBuilder.appendMessageDelta(items, '| ACL | 访问控制列表 |\n');
    ChatTimelineBuilder.appendMessageDelta(
      items,
      'Final Answer: ```echartsoption',
    );

    expect(items).toHaveLength(1);
    if (items[0]?.type === 'message') {
      expect(items[0].status).toBe('streaming');
      expect(items[0].parts.map((part) => part.type)).toEqual([
        'thinking',
        'text',
      ]);
      expect(items[0].parts[1]?.content).toBe(
        '## 标题\n| 模型 | 说明 |\n| --- | --- |\n| ACL | 访问控制列表 |\nFinal Answer: ```echartsoption',
      );
    }
  });

  it('appends thinking delta without accumulated snapshot replacement', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendThinkingDelta(
      items,
      '用户问的是“暑假安排是什么”。',
    );
    ChatTimelineBuilder.appendThinkingDelta(
      items,
      '用户问的是“暑假安排是什么”。我需要先检索知识库，看看有没有相关文档。',
    );
    ChatTimelineBuilder.appendMessageDelta(items, '正文');

    expect(items).toHaveLength(1);
    if (items[0]?.type === 'message') {
      expect(items[0].parts.map((part) => part.type)).toEqual([
        'thinking',
        'text',
      ]);
      expect(items[0].parts[0]?.content).toBe(
        '用户问的是“暑假安排是什么”。用户问的是“暑假安排是什么”。我需要先检索知识库，看看有没有相关文档。',
      );
      expect(items[0].parts[1]?.content).toBe('正文');
    }
  });

  it('ignores late thinking delta after text started in the same assistant message', () => {
    const items: ChatTimelineItem[] = [];

    ChatTimelineBuilder.appendThinkingDelta(items, '思考 A');
    ChatTimelineBuilder.appendMessageDelta(items, '正文');
    ChatTimelineBuilder.appendThinkingDelta(items, '思考 B');

    expect(items).toHaveLength(1);
    if (items[0]?.type === 'message') {
      expect(items[0].parts.map((part) => part.type)).toEqual([
        'thinking',
        'text',
      ]);
      expect(items[0].parts[0]?.content).toBe('思考 A');
      expect(items[0].parts[1]?.content).toBe('正文');
    }
  });
});
