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

import { EasyFlowAguiClient, EasyFlowAguiProjectionError } from './client';
import { isRetryableAguiTransportError } from './reconnect';

vi.mock('#/api/request', () => ({
  createEventStreamHeaders: () => ({ 'easyflow-token': 'test-token' }),
  resolveApiUrl: (url: string) => `http://localhost${url}`,
}));

function sse(events: unknown[]) {
  return new Response(
    events
      .map(
        (event, index) =>
          `id: ${index + 1}\ndata: ${JSON.stringify(event)}\n\n`,
      )
      .join(''),
    { headers: { 'Content-Type': 'text/event-stream' }, status: 200 },
  );
}

describe('easyFlowAguiClient', () => {
  afterEach(() => {
    vi.unstubAllGlobals();
    vi.useRealTimers();
  });

  it('trims outbound history, tools, context and state at the transport boundary', async () => {
    let requestBody: Record<string, unknown> | undefined;
    const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
      requestBody = JSON.parse(String(init?.body));
      return sse([
        { runId: 'run-1', threadId: '101', type: EventType.RUN_STARTED },
        {
          runId: 'run-1',
          threadId: '101',
          type: EventType.RUN_FINISHED,
        },
      ]);
    });
    vi.stubGlobal('fetch', fetchMock);

    const received: string[] = [];
    const cursors: number[] = [];
    await new EasyFlowAguiClient().run({
      forwardedProps: { easyflow: { input: { imageUploadIds: ['image-1'] } } },
      onEvent: (event) => {
        received.push(event.type);
      },
      onCursor: (cursor) => {
        cursors.push(cursor);
      },
      threadId: '101',
      url: '/api/v1/agent/1/agui/run',
      userMessage: { content: '你好', id: 'user-1', role: 'user' },
    });

    expect(received).toEqual([EventType.RUN_STARTED, EventType.RUN_FINISHED]);
    expect(cursors).toEqual([1, 2]);
    expect(requestBody).toEqual(
      expect.objectContaining({
        context: [],
        messages: [{ content: '你好', id: 'user-1', role: 'user' }],
        state: {},
        tools: [],
      }),
    );
  });

  it('converts proxied forwardedProps to transport JSON before the SDK clones input', async () => {
    let requestBody: Record<string, unknown> | undefined;
    vi.stubGlobal(
      'fetch',
      vi.fn(async (_url: string, init?: RequestInit) => {
        requestBody = JSON.parse(String(init?.body));
        return sse([
          { runId: 'run-1', threadId: 'draft-1', type: EventType.RUN_STARTED },
          {
            runId: 'run-1',
            threadId: 'draft-1',
            type: EventType.RUN_FINISHED,
          },
        ]);
      }),
    );
    const forwardedProps = new Proxy(
      { easyflow: { draft: { agent: { id: 'agent-1' } } } },
      {},
    );

    await expect(
      new EasyFlowAguiClient().run({
        forwardedProps,
        onEvent: () => undefined,
        threadId: 'draft-1',
        url: '/api/v1/agent/agui/run/draft',
        userMessage: { content: '你好', id: 'user-1', role: 'user' },
      }),
    ).resolves.toBeUndefined();
    expect(requestBody?.forwardedProps).toEqual(forwardedProps);
  });

  it('rejects a clean EOF without RUN_FINISHED or RUN_ERROR', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(async () =>
        sse([{ runId: 'run-1', threadId: '101', type: EventType.RUN_STARTED }]),
      ),
    );

    await expect(
      new EasyFlowAguiClient().run({
        onEvent: () => undefined,
        threadId: '101',
        url: '/api/v1/agent/1/agui/run',
        userMessage: { content: '你好', id: 'user-1', role: 'user' },
      }),
    ).rejects.toThrow('缺少终态');
  });

  it('marks deterministic event projection failures as non-retryable', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(async () =>
        sse([{ runId: 'run-1', threadId: '101', type: EventType.RUN_STARTED }]),
      ),
    );

    const run = new EasyFlowAguiClient().run({
      onEvent: () => {
        throw new Error('projection failed');
      },
      threadId: '101',
      url: '/api/v1/agent/1/agui/run',
      userMessage: { content: '你好', id: 'user-1', role: 'user' },
    });

    await expect(run).rejects.toBeInstanceOf(EasyFlowAguiProjectionError);
    expect(
      isRetryableAguiTransportError(
        new EasyFlowAguiProjectionError(new Error('projection failed')),
        Date.now(),
      ),
    ).toBe(false);
  });

  it('treats a standard cancelled terminal as an accepted stop', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(async () =>
        sse([
          { runId: 'run-1', threadId: '101', type: EventType.RUN_STARTED },
          {
            code: 'RUN_CANCELLED',
            message: '用户拒绝执行',
            runId: 'run-1',
            threadId: '101',
            type: EventType.RUN_ERROR,
          },
        ]),
      ),
    );

    await expect(
      new EasyFlowAguiClient().run({
        onEvent: () => undefined,
        threadId: '101',
        url: '/api/v1/agent/1/agui/run',
        userMessage: { content: '你好', id: 'user-1', role: 'user' },
      }),
    ).resolves.toBeUndefined();
  });

  it('does not hide projection errors raised by a cancelled terminal', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(async () =>
        sse([
          {
            code: 'RUN_CANCELLED',
            message: '用户停止',
            runId: 'run-1',
            threadId: '101',
            type: EventType.RUN_ERROR,
          },
        ]),
      ),
    );

    await expect(
      new EasyFlowAguiClient().run({
        onEvent: () => {
          throw new Error('cancel projection failed');
        },
        threadId: '101',
        url: '/api/v1/agent/1/agui/run',
        userMessage: { content: '你好', id: 'user-1', role: 'user' },
      }),
    ).rejects.toBeInstanceOf(EasyFlowAguiProjectionError);
  });

  it('yields a paint opportunity before consuming events after tool start', async () => {
    vi.useFakeTimers();
    let paintCallback: FrameRequestCallback | undefined;
    vi.stubGlobal(
      'requestAnimationFrame',
      vi.fn((callback: FrameRequestCallback) => {
        paintCallback = callback;
        return 1;
      }),
    );
    vi.stubGlobal(
      'fetch',
      vi.fn(async () =>
        sse([
          { runId: 'run-1', threadId: '101', type: EventType.RUN_STARTED },
          {
            runId: 'run-1',
            threadId: '101',
            toolCallId: 'tool-1',
            toolCallName: 'write_text_file',
            type: EventType.TOOL_CALL_START,
          },
          {
            delta: '{}',
            runId: 'run-1',
            threadId: '101',
            toolCallId: 'tool-1',
            type: EventType.TOOL_CALL_ARGS,
          },
          {
            runId: 'run-1',
            threadId: '101',
            toolCallId: 'tool-1',
            type: EventType.TOOL_CALL_END,
          },
          {
            content: 'done',
            messageId: 'tool-result-1',
            role: 'tool',
            runId: 'run-1',
            threadId: '101',
            toolCallId: 'tool-1',
            type: EventType.TOOL_CALL_RESULT,
          },
          {
            runId: 'run-1',
            threadId: '101',
            type: EventType.RUN_FINISHED,
          },
        ]),
      ),
    );
    const received: string[] = [];

    const runPromise = new EasyFlowAguiClient().run({
      onEvent: (event) => {
        received.push(event.type);
      },
      threadId: '101',
      url: '/api/v1/agent/1/agui/run',
      userMessage: { content: '生成文件', id: 'user-1', role: 'user' },
    });
    await vi.advanceTimersByTimeAsync(0);

    expect(received).toContain(EventType.TOOL_CALL_START);
    expect(received).not.toContain(EventType.TOOL_CALL_RESULT);

    paintCallback?.(0);
    await vi.advanceTimersByTimeAsync(0);
    await runPromise;

    expect(received).toContain(EventType.TOOL_CALL_RESULT);
    expect(received.at(-1)).toBe(EventType.RUN_FINISHED);
  });

  it('replays a completed run from the server journal after refresh', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(
        async () =>
          new Response(
            [
              'id: 1\ndata: {"type":"RUN_STARTED","threadId":"101","runId":"run-resume"}\n\n',
              'id: 2\ndata: {"type":"TEXT_MESSAGE_START","messageId":"assistant-1","role":"assistant"}\n\n',
              'id: 3\ndata: {"type":"TEXT_MESSAGE_CONTENT","messageId":"assistant-1","delta":"刷新后恢复"}\n\n',
              'id: 4\ndata: {"type":"TEXT_MESSAGE_END","messageId":"assistant-1"}\n\n',
              'id: 5\ndata: {"type":"RUN_FINISHED","threadId":"101","runId":"run-resume"}\n\n',
            ].join(''),
            { headers: { 'Content-Type': 'text/event-stream' }, status: 200 },
          ),
      ),
    );
    const received: string[] = [];
    const cursors: number[] = [];

    await new EasyFlowAguiClient().resume({
      onCursor: (cursor) => {
        cursors.push(cursor);
      },
      onEvent: (event) => {
        received.push(event.type);
      },
      runId: 'run-resume',
    });

    expect(received).toEqual([
      EventType.RUN_STARTED,
      EventType.TEXT_MESSAGE_START,
      EventType.TEXT_MESSAGE_CONTENT,
      EventType.TEXT_MESSAGE_END,
      EventType.RUN_FINISHED,
    ]);
    expect(cursors).toEqual([1, 2, 3, 4, 5]);
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining('/run-resume/events?after=0'),
      expect.objectContaining({ method: 'GET' }),
    );
  });

  it('uses the explicit cancel endpoint instead of treating detach as cancel', async () => {
    const fetchMock = vi.fn(
      async () =>
        new Response(JSON.stringify({ errorCode: 0 }), {
          headers: { 'Content-Type': 'application/json' },
          status: 200,
        }),
    );
    vi.stubGlobal('fetch', fetchMock);

    await new EasyFlowAguiClient().cancel('run-cancel');

    expect(fetchMock).toHaveBeenCalledWith(
      expect.stringContaining('/run-cancel/cancel'),
      expect.objectContaining({ method: 'POST' }),
    );
  });

  it('keeps the active stream attached when the cancel request fails', async () => {
    let runSignal: AbortSignal | undefined;
    const fetchMock = vi.fn((url: string, init?: RequestInit) => {
      if (url.includes('/cancel')) {
        return Promise.resolve(
          new Response(JSON.stringify({ message: '取消服务暂不可用' }), {
            headers: { 'Content-Type': 'application/json' },
            status: 503,
          }),
        );
      }
      runSignal = init?.signal as AbortSignal;
      return new Promise<Response>((_resolve, reject) => {
        runSignal?.addEventListener('abort', () =>
          reject(new DOMException('Aborted', 'AbortError')),
        );
      });
    });
    vi.stubGlobal('fetch', fetchMock);
    const client = new EasyFlowAguiClient();
    const runPromise = client.run({
      onEvent: () => undefined,
      runId: 'run-cancel-failure',
      threadId: '101',
      url: '/api/v1/agent/1/agui/run',
      userMessage: { content: '你好', id: 'user-1', role: 'user' },
    });

    await expect(client.cancel('run-cancel-failure')).rejects.toThrow(
      '取消服务暂不可用',
    );
    expect(runSignal?.aborted).toBe(false);

    client.detach();
    expect(runSignal?.aborted).toBe(true);
    await runPromise;
  });
});
