import type { ChatTimelineItem } from '@easyflow/common-ui';

import { EventSchemas, EventType } from '@ag-ui/client';
import { describe, expect, it } from 'vitest';

import { easyFlowAguiCustomEvent } from './custom-events';
import {
  applyAguiEventToTimeline,
  createAguiTimelineProjectionState,
} from './projection';

describe('aG-UI wire contract and timeline projection', () => {
  it('projects a published Artifact through safe public fields only', () => {
    const items: ChatTimelineItem[] = [];
    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        name: easyFlowAguiCustomEvent.artifactPublished,
        type: EventType.CUSTOM,
        value: {
          artifactId: '01JARTIFACT',
          bucket: 'private-bucket',
          downloadUrl: 'https://evil.example/file',
          fileName: '../项目报告.pdf',
          mimeType: 'application/pdf',
          objectKey: 'formal/tenant/secret',
          schemaVersion: 1,
          sha256: 'A'.repeat(64),
          size: 2048,
          status: 'AVAILABLE',
          workspacePath: '/app/data/agent-workspaces/private/report.pdf',
        },
      }),
      { roundId: 'round-artifact' },
    );

    expect(items).toEqual([
      expect.objectContaining({
        artifactId: '01JARTIFACT',
        downloadUrl: '/api/v1/agent/artifacts/01JARTIFACT/content',
        fileName: '项目报告.pdf',
        roundId: 'round-artifact',
        sha256: 'a'.repeat(64),
        status: 'available',
        type: 'artifact',
      }),
    ]);
    expect(JSON.stringify(items)).not.toMatch(
      /private-bucket|evil\.example|objectKey|workspacePath|agent-workspaces/,
    );
  });

  it('accepts representative Java wire events with the official schemas', () => {
    const events = [
      {
        runId: 'run-1',
        threadId: '101',
        type: EventType.RUN_STARTED,
      },
      {
        messageId: 'reasoning-1',
        role: 'reasoning',
        runId: 'run-1',
        threadId: '101',
        type: EventType.REASONING_MESSAGE_START,
      },
      {
        messageId: 'assistant-1',
        role: 'assistant',
        runId: 'run-1',
        threadId: '101',
        type: EventType.TEXT_MESSAGE_START,
      },
      {
        delta: '你好',
        messageId: 'assistant-1',
        runId: 'run-1',
        threadId: '101',
        type: EventType.TEXT_MESSAGE_CONTENT,
      },
      {
        name: easyFlowAguiCustomEvent.inputAccepted,
        runId: 'run-1',
        threadId: '101',
        type: EventType.CUSTOM,
        value: { schemaVersion: 1 },
      },
      {
        runId: 'run-1',
        threadId: '101',
        type: EventType.RUN_FINISHED,
      },
    ];

    expect(events.map((event) => EventSchemas.parse(event))).toHaveLength(6);
  });

  it('creates a running turn as soon as RUN_STARTED arrives', () => {
    const items: ChatTimelineItem[] = [];
    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        runId: 'run-started',
        threadId: 'thread-started',
        type: EventType.RUN_STARTED,
      }),
      { roundId: 'round-started', startedAt: 1000 },
    );

    expect(items).toHaveLength(1);
    expect(items[0]).toMatchObject({
      parts: [],
      role: 'assistant',
      roundId: 'round-started',
      status: 'streaming',
      turnStartedAt: 1000,
      type: 'message',
    });
  });

  it('projects standard and custom events without a local standard enum', () => {
    const items: ChatTimelineItem[] = [];
    const state = createAguiTimelineProjectionState();
    const events = [
      {
        delta: '分析中',
        messageId: 'reasoning-1',
        type: EventType.REASONING_MESSAGE_CONTENT,
      },
      {
        delta: '执行结果',
        messageId: 'assistant-1',
        type: EventType.TEXT_MESSAGE_CONTENT,
      },
      {
        toolCallId: 'tool-1',
        toolCallName: 'workflow',
        type: EventType.TOOL_CALL_START,
      },
      {
        name: easyFlowAguiCustomEvent.toolMetadata,
        type: EventType.CUSTOM,
        value: {
          toolCallId: 'tool-1',
          toolDisplayName: '数据处理工作流',
          toolName: 'workflow',
        },
      },
      {
        delta: '{"topic":"AG-UI"}',
        toolCallId: 'tool-1',
        type: EventType.TOOL_CALL_ARGS,
      },
      {
        content: '完成',
        messageId: 'tool-result-1',
        role: 'tool',
        toolCallId: 'tool-1',
        type: EventType.TOOL_CALL_RESULT,
      },
      {
        name: easyFlowAguiCustomEvent.toolApprovalRequired,
        type: EventType.CUSTOM,
        value: {
          approvalId: 'approval-public',
          toolCallId: 'tool-2',
          toolName: 'dangerous_tool',
        },
      },
      {
        name: easyFlowAguiCustomEvent.toolApprovalResolved,
        type: EventType.CUSTOM,
        value: {
          approvalId: 'approval-public',
          reason: '用户拒绝执行',
          status: 'REJECTED',
        },
      },
      {
        runId: 'run-1',
        threadId: '101',
        type: EventType.RUN_FINISHED,
      },
    ].map((event) => EventSchemas.parse(event));

    for (const event of events) {
      applyAguiEventToTimeline(items, event, { roundId: 'round-1' }, state);
    }

    expect(JSON.stringify(items)).toContain('分析中');
    expect(JSON.stringify(items)).toContain('执行结果');
    expect(items.find((item) => item.id === 'tool-1')).toMatchObject({
      input: { topic: 'AG-UI' },
      output: '完成',
      status: 'success',
      toolName: '数据处理工作流',
    });
    const approval = items.find((item) => item.id === 'tool-2');
    expect(approval?.type === 'tool' && approval.approval).toEqual(
      expect.objectContaining({ approvalId: 'approval-public' }),
    );
    expect(approval).toMatchObject({ status: 'rejected' });
    expect(JSON.stringify(approval)).not.toContain('resumeToken');
  });

  it('uses standard message snapshots for authoritative text and keeps cancellation non-error', () => {
    const items: ChatTimelineItem[] = [];
    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        delta: 'draft',
        messageId: 'assistant-1',
        type: EventType.TEXT_MESSAGE_CONTENT,
      }),
    );
    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        messages: [
          { content: 'question', id: 'user-1', role: 'user' },
          { content: 'final', id: 'assistant-1', role: 'assistant' },
        ],
        runId: 'run-1',
        threadId: '101',
        type: EventType.MESSAGES_SNAPSHOT,
      }),
    );
    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        code: 'RUN_CANCELLED',
        message: '用户拒绝执行',
        runId: 'run-1',
        threadId: '101',
        type: EventType.RUN_ERROR,
      }),
    );

    expect(JSON.stringify(items)).toContain('final');
    expect(JSON.stringify(items)).not.toContain('draft');
    expect(items.some((item) => item.type === 'error')).toBe(false);
  });

  it('aligns message snapshots by message id and supports an empty authoritative body', () => {
    const items: ChatTimelineItem[] = [];
    const state = createAguiTimelineProjectionState();
    for (const event of [
      {
        delta: '第一段',
        messageId: 'assistant-1',
        type: EventType.TEXT_MESSAGE_CONTENT,
      },
      {
        toolCallId: 'tool-1',
        toolCallName: 'workflow',
        type: EventType.TOOL_CALL_START,
      },
      {
        delta: '过期正文',
        messageId: 'assistant-2',
        type: EventType.TEXT_MESSAGE_CONTENT,
      },
    ].map((event) => EventSchemas.parse(event))) {
      applyAguiEventToTimeline(items, event, { roundId: 'round-1' }, state);
    }

    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        messages: [{ content: '', id: 'assistant-2', role: 'assistant' }],
        runId: 'run-1',
        threadId: '101',
        type: EventType.MESSAGES_SNAPSHOT,
      }),
      { roundId: 'round-1' },
      state,
    );

    const assistants = items.filter(
      (item) => item.type === 'message' && item.role === 'assistant',
    );
    expect(assistants).toHaveLength(1);
    expect(assistants[0]).toMatchObject({ id: 'assistant-2', status: 'done' });
    expect(JSON.stringify(assistants[0])).not.toContain('过期正文');
  });

  it('projects one AG-UI run as a completed timeline turn', () => {
    const items: ChatTimelineItem[] = [];
    const state = createAguiTimelineProjectionState();
    const events = [
      {
        runId: 'run-1',
        threadId: 'thread-1',
        type: EventType.RUN_STARTED,
      },
      {
        toolCallId: 'tool-1',
        toolCallName: 'context7',
        type: EventType.TOOL_CALL_START,
      },
      {
        content: 'ok',
        messageId: 'tool-result-1',
        role: 'tool',
        toolCallId: 'tool-1',
        type: EventType.TOOL_CALL_RESULT,
      },
      {
        delta: '最终回答',
        messageId: 'assistant-1',
        type: EventType.TEXT_MESSAGE_CONTENT,
      },
      {
        runId: 'run-1',
        threadId: 'thread-1',
        type: EventType.RUN_FINISHED,
      },
    ].map((event) => EventSchemas.parse(event));

    for (const event of events) {
      applyAguiEventToTimeline(items, event, { roundId: 'round-1' }, state);
    }

    expect(items).toHaveLength(2);
    expect(items.every((item) => item.roundId === 'round-1')).toBe(true);
    expect(items.every((item) => item.turnSucceeded === true)).toBe(true);
    expect(items.every((item) => item.turnStartedAt !== undefined)).toBe(true);
    expect(items.every((item) => item.turnFinishedAt !== undefined)).toBe(true);
    expect(
      items.some(
        (item) =>
          item.type === 'message' &&
          item.role === 'assistant' &&
          item.roundCompleted,
      ),
    ).toBe(true);
  });

  it('projects Skill invocation status in place through the strict public fields', () => {
    const items: ChatTimelineItem[] = [];
    const state = createAguiTimelineProjectionState();
    for (const event of [
      {
        name: easyFlowAguiCustomEvent.skillInvocationStatus,
        type: EventType.CUSTOM,
        value: {
          configJson: { token: 'secret' },
          input: { contract: 'private' },
          path: 'references/private.md',
          skillContent: 'private Skill body',
          skillDisplayName: '合同审查助手',
          skillId: '101',
          skillName: 'contract-review',
          status: 'RUNNING',
          statusKey: 'skill-invocation:round-skill:101',
          toolCallId: 'tool-skill-1',
        },
      },
      {
        name: easyFlowAguiCustomEvent.skillInvocationStatus,
        type: EventType.CUSTOM,
        value: {
          message: '完成',
          skillDisplayName: '合同审查助手',
          skillId: '101',
          skillName: 'contract-review',
          status: 'SUCCESS',
          statusKey: 'skill-invocation:round-skill:101',
          toolCallId: 'tool-skill-1',
        },
      },
    ].map((event) => EventSchemas.parse(event))) {
      applyAguiEventToTimeline(items, event, { roundId: 'round-skill' }, state);
    }

    expect(items).toHaveLength(1);
    expect(items[0]).toMatchObject({
      icon: 'skill',
      label: '已调用 合同审查助手',
      roundId: 'round-skill',
      status: 'done',
      statusKey: 'skill-invocation:round-skill:101',
      type: 'status',
    });
    expect(JSON.stringify(items[0])).not.toMatch(
      /skillContent|private Skill body|private\.md|contract|token|toolCallId/,
    );
  });

  it.each([
    ['FAILED', 'error', '调用 合同审查助手 失败'],
    ['CANCELLED', 'cancelled', '已停止调用 合同审查助手'],
    ['INCOMPLETE', 'incomplete', '调用 合同审查助手 未完成'],
  ] as const)(
    'keeps the %s Skill terminal semantics',
    (status, expected, label) => {
      const items: ChatTimelineItem[] = [];
      applyAguiEventToTimeline(
        items,
        EventSchemas.parse({
          name: easyFlowAguiCustomEvent.skillInvocationStatus,
          type: EventType.CUSTOM,
          value: {
            skillDisplayName: '合同审查助手',
            status,
            statusKey: 'skill-invocation:r1:101',
          },
        }),
        { roundId: 'r1' },
      );

      expect(items[0]).toMatchObject({ label, status: expected });
    },
  );

  it('closes a running Skill as cancelled when the run is cancelled', () => {
    const items: ChatTimelineItem[] = [];
    const state = createAguiTimelineProjectionState();
    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        name: easyFlowAguiCustomEvent.skillInvocationStatus,
        type: EventType.CUSTOM,
        value: {
          skillDisplayName: '合同审查助手',
          status: 'RUNNING',
          statusKey: 'skill-invocation:r1:101',
        },
      }),
      { roundId: 'r1' },
      state,
    );
    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        code: 'RUN_CANCELLED',
        message: 'cancelled',
        runId: 'run-1',
        threadId: 'thread-1',
        type: EventType.RUN_ERROR,
      }),
      { roundId: 'r1' },
      state,
    );

    expect(items[0]).toMatchObject({
      label: '已停止调用 合同审查助手',
      status: 'cancelled',
    });
  });

  it('closes a running Skill as incomplete when the run finishes without its terminal event', () => {
    const items: ChatTimelineItem[] = [];
    const state = createAguiTimelineProjectionState();
    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        name: easyFlowAguiCustomEvent.skillInvocationStatus,
        type: EventType.CUSTOM,
        value: {
          skillDisplayName: '合同审查助手',
          status: 'RUNNING',
          statusKey: 'skill-invocation:r1:101',
        },
      }),
      { roundId: 'r1' },
      state,
    );
    applyAguiEventToTimeline(
      items,
      EventSchemas.parse({
        runId: 'run-1',
        threadId: 'thread-1',
        type: EventType.RUN_FINISHED,
      }),
      { roundId: 'r1' },
      state,
    );

    expect(items[0]).toMatchObject({
      label: '调用 合同审查助手 未完成',
      status: 'incomplete',
    });
  });
});
