// @vitest-environment happy-dom

import type {
  EasyFlowAguiResumeOptions,
  EasyFlowAguiRunOptions,
} from '../../shared/agent-agui/client';
import type { AgentInfo } from '../types';

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

import { easyFlowAguiCustomEvent } from '../../shared/agent-agui/custom-events';
import { useAgentTryoutStream } from './useAgentTryoutStream';

const aguiMocks = vi.hoisted(() => ({
  abort: vi.fn(),
  cancel: vi.fn(async () => undefined),
  detach: vi.fn(),
  resume: vi.fn(),
  run: vi.fn(),
  runs: [] as EasyFlowAguiRunOptions[],
}));

vi.mock('../../shared/agent-agui/client', () => ({
  createAguiRunId: () => 'run-test',
  EasyFlowAguiHttpError: class extends Error {
    constructor(
      message: string,
      public readonly status: number,
    ) {
      super(message);
    }
  },
  EasyFlowAguiProjectionError: class extends Error {
    constructor(public readonly projectionCause: unknown) {
      super('Agent 事件投影失败');
    }
  },
  EasyFlowAguiClient: class {
    abort = aguiMocks.abort;
    cancel = aguiMocks.cancel;
    detach = aguiMocks.detach;
    resume = aguiMocks.resume;

    run(options: EasyFlowAguiRunOptions) {
      aguiMocks.runs.push(options);
      return aguiMocks.run(options);
    }
  },
}));

vi.mock('../api', () => ({
  clearAgentDraftSession: vi.fn(async () => ({ errorCode: 0 })),
}));

describe('useAgentTryoutStream', () => {
  beforeEach(() => {
    sessionStorage.clear();
    aguiMocks.runs.length = 0;
    vi.resetAllMocks();
    aguiMocks.cancel.mockResolvedValue(undefined);
    aguiMocks.run.mockImplementation(() => new Promise(() => {}));
    aguiMocks.resume.mockImplementation(() => new Promise(() => {}));
  });

  it('ignores late events from a stopped draft run after resending', async () => {
    aguiMocks.resume.mockImplementationOnce(
      async (options: EasyFlowAguiResumeOptions) => {
        options.onEvent({
          code: 'RUN_CANCELLED',
          message: '用户已停止生成',
          runId: 'run-test',
          threadId: 'draft-race',
          type: EventType.RUN_ERROR,
        });
      },
    );
    const stream = useAgentTryoutStream();
    const payload = {
      agent: { id: 'agent-1', name: 'Agent' } as AgentInfo,
      knowledgeBindings: [],
      prompt: '旧问题',
      sessionId: 'draft-race',
      skillBindings: [],
      toolBindings: [],
    };

    void stream.sendDraft(payload);
    await Promise.resolve();
    await stream.stop();
    void stream.sendDraft({ ...payload, prompt: '新问题' });
    await Promise.resolve();

    aguiMocks.runs[0]?.onEvent({
      delta: '旧流迟到正文',
      messageId: 'old-assistant',
      type: EventType.TEXT_MESSAGE_CONTENT,
    });
    aguiMocks.runs[1]?.onEvent({
      delta: '新流正文',
      messageId: 'new-assistant',
      type: EventType.TEXT_MESSAGE_CONTENT,
    });

    const timeline = JSON.stringify(stream.timelineItems.value);
    expect(timeline).toContain('新流正文');
    expect(timeline).not.toContain('旧流迟到正文');
  });

  it('sends only the draft runtime snapshot through forwardedProps', async () => {
    const stream = useAgentTryoutStream();
    void stream.sendDraft({
      agent: {
        created: '2026-08-14 10:00:00',
        displayPublishStatus: 'PUBLISHED',
        id: 'agent-1',
        modelId: 'model-1',
        name: 'Agent',
        publishedSnapshotJson: { internal: true },
      },
      knowledgeBindings: [
        {
          agentId: 'agent-1',
          enabled: true,
          id: 'knowledge-binding-1',
          knowledgeId: 'knowledge-1',
          localId: 'local-knowledge',
          optionsJson: { topK: 5 },
          resourceSnapshot: { internal: true },
          resourceSummary: { displayName: '知识库' },
          retrievalMode: 'HYBRID',
          sortNo: 2,
        },
      ],
      prompt: '问题',
      sessionId: 'draft-snapshot',
      skillBindings: [
        {
          agentId: 'agent-1',
          id: 'skill-binding-1',
          resourceSummary: {
            displayName: '合同审查',
            toolCount: 2,
          },
          skillId: 'skill-1',
          sortNo: 3,
        },
      ],
      toolBindings: [
        {
          agentId: 'agent-1',
          enabled: true,
          hitlConfigJson: { message: '请确认' },
          hitlEnabled: true,
          id: 'tool-binding-1',
          localId: 'local-tool',
          optionsJson: { timeout: 30 },
          resourceSnapshot: { internal: true },
          resourceSummary: { displayName: '工具' },
          sortNo: 1,
          targetId: 'tool-1',
          toolName: 'search',
          toolType: 'PLUGIN',
        },
      ],
    });
    await Promise.resolve();

    const forwardedProps = aguiMocks.runs[0]?.forwardedProps as {
      easyflow: {
        draft: {
          agent: Record<string, unknown>;
          knowledgeBindings: Record<string, unknown>[];
          skillBindings: Record<string, unknown>[];
          toolBindings: Record<string, unknown>[];
        };
      };
    };
    expect(forwardedProps.easyflow.draft.agent).toEqual(
      expect.objectContaining({
        id: 'agent-1',
        modelId: 'model-1',
        name: 'Agent',
      }),
    );
    expect(forwardedProps.easyflow.draft.agent).not.toHaveProperty('created');
    expect(forwardedProps.easyflow.draft.agent).not.toHaveProperty(
      'publishedSnapshotJson',
    );
    expect(forwardedProps.easyflow.draft.toolBindings).toEqual([
      {
        enabled: true,
        hitlConfigJson: { message: '请确认' },
        hitlEnabled: true,
        id: 'tool-binding-1',
        optionsJson: { timeout: 30 },
        sortNo: 1,
        targetId: 'tool-1',
        toolName: 'search',
        toolType: 'PLUGIN',
      },
    ]);
    expect(forwardedProps.easyflow.draft.knowledgeBindings).toEqual([
      {
        enabled: true,
        id: 'knowledge-binding-1',
        knowledgeId: 'knowledge-1',
        optionsJson: { topK: 5 },
        retrievalMode: 'HYBRID',
        sortNo: 2,
      },
    ]);
    expect(forwardedProps.easyflow.draft.skillBindings).toEqual([
      {
        skillId: 'skill-1',
        sortNo: 3,
      },
    ]);
    aguiMocks.resume.mockImplementationOnce(
      async (options: EasyFlowAguiResumeOptions) => {
        options.onEvent({
          code: 'RUN_CANCELLED',
          message: '用户已停止生成',
          runId: 'run-test',
          threadId: 'draft-snapshot',
          type: EventType.RUN_ERROR,
        });
      },
    );
    await stream.stop();
  });

  it('刷新跨过输入确认事件后会幂等清理恢复的草稿', async () => {
    const payload = {
      agent: { id: 'agent-accepted', name: 'Agent' } as AgentInfo,
      knowledgeBindings: [],
      prompt: '带附件的问题',
      sessionId: 'draft-accepted-restore',
      skillBindings: [],
      toolBindings: [],
    };
    const firstStream = useAgentTryoutStream();
    void firstStream.sendDraft(payload);
    await Promise.resolve();
    aguiMocks.runs[0]?.onEvent({
      name: easyFlowAguiCustomEvent.inputAccepted,
      type: EventType.CUSTOM,
      value: {},
    });
    aguiMocks.runs[0]?.onCursor?.(4);
    firstStream.dispose();

    const restoredAccepted = vi.fn();
    const restoredStream = useAgentTryoutStream();
    restoredStream.syncDraftContext(
      payload,
      true,
      payload.sessionId,
      restoredAccepted,
    );
    await Promise.resolve();

    expect(restoredAccepted).toHaveBeenCalledTimes(1);
    expect(aguiMocks.resume).toHaveBeenCalledWith(
      expect.objectContaining({ afterCursor: 4, runId: 'run-test' }),
    );
    aguiMocks.resume.mockImplementationOnce(
      async (options: EasyFlowAguiResumeOptions) => {
        options.onEvent({
          code: 'RUN_CANCELLED',
          message: '用户已停止生成',
          runId: 'run-test',
          threadId: 'draft-accepted-restore',
          type: EventType.RUN_ERROR,
        });
      },
    );
    await restoredStream.stop();
  });

  it('草稿流短暂断网后从最后游标恢复且不伪造本地失败', async () => {
    vi.useFakeTimers();
    aguiMocks.run.mockRejectedValueOnce(new TypeError('Failed to fetch'));
    aguiMocks.resume
      .mockRejectedValueOnce(new TypeError('Network unavailable'))
      .mockRejectedValueOnce(new TypeError('Network unavailable'))
      .mockImplementationOnce(async (options: EasyFlowAguiResumeOptions) => {
        await options.onEvent({
          runId: 'run-test',
          threadId: 'draft-network-recovery',
          type: EventType.RUN_FINISHED,
        });
      });
    const stream = useAgentTryoutStream();
    const runPromise = stream.sendDraft({
      agent: { id: 'agent-network', name: 'Agent' } as AgentInfo,
      knowledgeBindings: [],
      prompt: '网络恢复',
      sessionId: 'draft-network-recovery',
      skillBindings: [],
      toolBindings: [],
    });

    await vi.advanceTimersByTimeAsync(1000);
    await runPromise;

    expect(aguiMocks.resume).toHaveBeenCalledTimes(3);
    expect(stream.loading.value).toBe(false);
    expect(JSON.stringify(stream.timelineItems.value)).not.toContain(
      '试运行失败',
    );
  });

  it('停止请求失败时不会解引用已清空运行并会重连服务端运行', async () => {
    aguiMocks.cancel.mockRejectedValueOnce(new Error('停止接口不可用'));
    aguiMocks.resume.mockImplementation(() => new Promise(() => {}));
    const stream = useAgentTryoutStream();
    void stream.sendDraft({
      agent: { id: 'agent-stop-failed', name: 'Agent' } as AgentInfo,
      knowledgeBindings: [],
      prompt: '停止失败恢复',
      sessionId: 'draft-stop-failed',
      skillBindings: [],
      toolBindings: [],
    });
    await Promise.resolve();

    await expect(stream.stop()).rejects.toThrow('停止接口不可用');

    expect(aguiMocks.detach).toHaveBeenCalled();
    expect(aguiMocks.resume).toHaveBeenCalledTimes(1);
    expect(stream.loading.value).toBe(true);
    expect(JSON.stringify(stream.timelineItems.value)).toContain(
      '停止失败恢复',
    );
    stream.dispose();
  });

  it('运行中拒绝清理会话且不取消服务端运行', async () => {
    const stream = useAgentTryoutStream();
    void stream.sendDraft({
      agent: { id: 'agent-clear-failed', name: 'Agent' } as AgentInfo,
      knowledgeBindings: [],
      prompt: '应保留的草稿',
      sessionId: 'draft-clear-failed',
      skillBindings: [],
      toolBindings: [],
    });
    await Promise.resolve();

    await expect(stream.clearDraftSession()).rejects.toThrow(
      '试运行进行中，暂时无法清理会话',
    );

    expect(aguiMocks.cancel).not.toHaveBeenCalled();
    expect(aguiMocks.resume).not.toHaveBeenCalled();
    expect(stream.loading.value).toBe(true);
    expect(JSON.stringify(stream.timelineItems.value)).toContain(
      '应保留的草稿',
    );
    stream.dispose();
  });

  it('取消响应晚于自然完成时保留权威完成态', async () => {
    let resolveCancel: ((value: undefined) => void) | undefined;
    let resolveRun: (() => void) | undefined;
    aguiMocks.cancel.mockImplementation(
      () =>
        new Promise<undefined>((resolve) => {
          resolveCancel = resolve;
        }),
    );
    aguiMocks.run.mockImplementation(
      () =>
        new Promise<void>((resolve) => {
          resolveRun = resolve;
        }),
    );
    const stream = useAgentTryoutStream();
    void stream.sendDraft({
      agent: { id: 'agent-stop-race', name: 'Agent' } as AgentInfo,
      knowledgeBindings: [],
      prompt: '自然完成优先',
      sessionId: 'draft-stop-terminal-race',
      skillBindings: [],
      toolBindings: [],
    });
    await Promise.resolve();

    const stopPromise = stream.stop();
    aguiMocks.runs[0]?.onEvent({
      runId: 'run-test',
      threadId: 'draft-stop-terminal-race',
      type: EventType.RUN_FINISHED,
    });
    aguiMocks.runs[0]?.onCursor?.(1);
    resolveRun?.();
    resolveCancel?.(undefined);
    await stopPromise;

    expect(stream.loading.value).toBe(false);
    expect(JSON.stringify(stream.timelineItems.value)).toContain(
      '"turnSucceeded":true',
    );
    expect(JSON.stringify(stream.timelineItems.value)).not.toContain(
      '"turnSucceeded":false',
    );
  });

  it('取消成功后重放权威终态且保留尚未投影的自然完成', async () => {
    aguiMocks.resume.mockImplementationOnce(
      async (options: EasyFlowAguiResumeOptions) => {
        options.onEvent({
          runId: 'run-test',
          threadId: 'draft-stop-authoritative',
          type: EventType.RUN_FINISHED,
        });
        options.onCursor?.(9);
      },
    );
    const stream = useAgentTryoutStream();
    void stream.sendDraft({
      agent: { id: 'agent-stop-authoritative', name: 'Agent' } as AgentInfo,
      knowledgeBindings: [],
      prompt: '取消时服务端已经自然完成',
      sessionId: 'draft-stop-authoritative',
      skillBindings: [],
      toolBindings: [],
    });
    await Promise.resolve();

    await stream.stop();

    expect(stream.loading.value).toBe(false);
    expect(JSON.stringify(stream.timelineItems.value)).toContain(
      '"turnSucceeded":true',
    );
    expect(JSON.stringify(stream.timelineItems.value)).not.toContain(
      '"turnSucceeded":false',
    );
  });
});
