"use client";

import { act, renderHook, waitFor } from "@testing-library/react";

import { useAgentChatSession } from "./useAgentChatSession";
import {
  abortAgentChat,
  forkAgentChat,
  replyAgentPermission,
  replyAgentQuestion,
  resumeAgentChatStream,
  streamAgentChat,
} from "@/lib/chatStream";
import type { StreamEvent } from "@/lib/chatStream";

jest.mock("@/lib/chatStream", () => ({
  abortAgentChat: jest.fn(async () => undefined),
  forkAgentChat: jest.fn(async () => "forked-session"),
  replyAgentPermission: jest.fn(async () => undefined),
  replyAgentQuestion: jest.fn(async () => undefined),
  resumeAgentChatStream: jest.fn(async () => undefined),
  streamAgentChat: jest.fn(async () => undefined),
}));

const listChatSessions = jest.fn();
const deleteChatSession = jest.fn();
const updateChatSessionTitle = jest.fn();

jest.mock("../chatStorage", () => ({
  createEmptyChatState: jest.fn(() => ({
    title: undefined,
    isTitleManuallyEdited: false,
    messages: [],
    sessionId: undefined,
  })),
  deleteChatSession: (...args: unknown[]) => deleteChatSession(...args),
  listChatSessions: (...args: unknown[]) => listChatSessions(...args),
  loadChatSessionById: jest.fn(async () => ({
    title: "已存在会话",
    isTitleManuallyEdited: false,
    messages: [],
    sessionId: "session-loaded",
  })),
  updateChatSessionTitle: (...args: unknown[]) => updateChatSessionTitle(...args),
}));

describe("useAgentChatSession", () => {
  beforeEach(() => {
    listChatSessions.mockReset();
    deleteChatSession.mockReset();
    updateChatSessionTitle.mockReset();
    jest.mocked(abortAgentChat).mockReset();
    jest.mocked(forkAgentChat).mockReset();
    jest.mocked(replyAgentPermission).mockReset();
    jest.mocked(replyAgentQuestion).mockReset();
    jest.mocked(resumeAgentChatStream).mockReset();
    jest.mocked(streamAgentChat).mockReset();
    jest.mocked(abortAgentChat).mockImplementation(async () => undefined);
    jest.mocked(forkAgentChat).mockImplementation(async () => "forked-session");
    jest.mocked(replyAgentPermission).mockImplementation(async () => undefined);
    jest.mocked(replyAgentQuestion).mockImplementation(async () => undefined);
    jest.mocked(resumeAgentChatStream).mockImplementation(async () => undefined);
    jest.mocked(streamAgentChat).mockImplementation(async () => undefined);
    deleteChatSession.mockImplementation(async () => undefined);
    updateChatSessionTitle.mockImplementation(async () => undefined);
  });

describe("useAgentChatSession lifecycle and resume", () => {
  it("does not add a new empty session to history until there is actual chat content", async () => {
    listChatSessions.mockResolvedValue([]);

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    act(() => {
      void result.current.createSession();
    });

    await waitFor(() => expect(result.current.sessionTitle).toBe("新对话"));
    expect(result.current.chatSessions).toEqual([]);
    expect(result.current.activeSessionId).toBeUndefined();
    expect(result.current.messages).toEqual([]);
    expect(result.current.isStreaming).toBe(false);
    expect(listChatSessions).toHaveBeenCalledTimes(1);
  });

  it("keeps existing history entries when creating a blank new session", async () => {
    listChatSessions.mockResolvedValue([
      {
        id: "session-1",
        title: "已有会话",
        createdAt: 1,
        updatedAt: 1,
      },
    ]);

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    act(() => {
      void result.current.createSession();
    });

    expect(result.current.chatSessions).toEqual([
      {
        id: "session-1",
        title: "已有会话",
        createdAt: 1,
        updatedAt: 1,
      },
    ]);
  });

  it("removes a deleted history entry before the backend delete finishes", async () => {
    const initialSessions = [
      {
        id: "session-1",
        title: "第一段会话",
        createdAt: 2,
        updatedAt: 2,
      },
      {
        id: "session-2",
        title: "第二段会话",
        createdAt: 1,
        updatedAt: 1,
      },
    ];
    let resolveDelete: ((nextActiveSessionId?: string) => void) | undefined;

    listChatSessions.mockResolvedValue(initialSessions);
    deleteChatSession.mockImplementationOnce(
      () =>
        new Promise<string | undefined>((resolve) => {
          resolveDelete = resolve;
        }),
    );

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    act(() => {
      void result.current.removeSession("session-2");
    });

    expect(result.current.chatSessions).toEqual([
      expect.objectContaining({ id: "session-1" }),
    ]);

    listChatSessions.mockResolvedValue([
      {
        id: "session-1",
        title: "第一段会话",
        createdAt: 2,
        updatedAt: 2,
      },
    ]);

    await act(async () => {
      resolveDelete?.();
      await Promise.resolve();
    });

    await waitFor(() =>
      expect(result.current.chatSessions).toEqual([
        expect.objectContaining({ id: "session-1" }),
      ]),
    );
  });

  it("does not autosave full messages after the stream is done", async () => {
    listChatSessions.mockResolvedValue([]);
    let emitStreamEvent: ((event: StreamEvent) => void) | undefined;
    jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => {
      emitStreamEvent = onEvent;
      await new Promise<void>(() => undefined);
    });

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    jest.useFakeTimers();
    try {
      await act(async () => {
        void result.current.sendPrompt("第一条消息");
        await Promise.resolve();
      });

      expect(result.current.isStreaming).toBe(true);

      await act(async () => {
        jest.advanceTimersByTime(200);
      });

      act(() => {
        emitStreamEvent?.({
          type: "token",
          sessionId: "chat-stream-1",
          content: "收到",
        });
      });

      await act(async () => {
        jest.advanceTimersByTime(200);
      });

      act(() => {
        emitStreamEvent?.({
          type: "done",
          sessionId: "chat-stream-1",
        });
      });

      await act(async () => {
        jest.advanceTimersByTime(200);
      });

      expect(result.current.messages).toEqual([
        expect.objectContaining({ role: "user", content: "第一条消息" }),
        expect.objectContaining({ role: "assistant", content: "收到" }),
      ]);
      expect(result.current.activeSessionId).toBe("chat-stream-1");
    } finally {
      jest.useRealTimers();
    }
  });

  it("shows shared todo state only on the latest assistant message in a session", async () => {
    listChatSessions.mockResolvedValue([]);
    jest.mocked(streamAgentChat)
      .mockImplementationOnce(async ({ onEvent }) => {
        onEvent({
          type: "todo_update",
          sessionId: "session-1",
          todos: [
            {
              id: "todo-1",
              content: "创建任务列表",
              status: "in_progress",
            },
          ],
          createdAt: 1000,
        });
        onEvent({
          type: "done",
          sessionId: "session-1",
        });
      })
      .mockImplementationOnce(async ({ onEvent }) => {
        onEvent({
          type: "todo_update",
          sessionId: "session-1",
          todos: [
            {
              id: "todo-1",
              content: "创建任务列表",
              status: "completed",
            },
            {
              id: "todo-2",
              content: "更新任务状态",
              status: "in_progress",
            },
          ],
          createdAt: 2000,
        });
        onEvent({
          type: "done",
          sessionId: "session-1",
        });
      });

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    await act(async () => {
      await result.current.sendPrompt("创建任务");
    });
    await waitFor(() => expect(result.current.isStreaming).toBe(false));

    await act(async () => {
      await result.current.sendPrompt("更新任务");
    });
    await waitFor(() => expect(result.current.isStreaming).toBe(false));

    const assistantMessages = result.current.messages.filter(
      (message) => message.role === "assistant",
    );

    expect(assistantMessages).toHaveLength(2);
    expect(assistantMessages[0].todos).toBeUndefined();
    expect(assistantMessages[1].todos).toEqual(
      expect.objectContaining({
        sessionId: "session-1",
        createdAt: 2000,
        todos: [
          expect.objectContaining({
            id: "todo-1",
            status: "completed",
          }),
          expect.objectContaining({
            id: "todo-2",
            status: "in_progress",
          }),
        ],
      }),
    );
  });

  it("hydrates a backend streaming session and resumes its stream", async () => {
    listChatSessions.mockResolvedValue([
      {
        id: "session-streaming",
        title: "运行中",
        createdAt: 1,
        updatedAt: 2,
        isStreaming: true,
        runStatus: "running",
      },
    ]);

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    expect(result.current.isStreaming).toBe(true);
    expect(result.current.activeSessionId).toBe("session-loaded");
    expect(resumeAgentChatStream).toHaveBeenCalledWith(
      expect.objectContaining({
        sessionId: "session-loaded",
      }),
    );
  });

  it("updates resumed messages from state, token, and done events", async () => {
    listChatSessions.mockResolvedValue([
      {
        id: "session-streaming",
        title: "运行中",
        createdAt: 1,
        updatedAt: 2,
        isStreaming: true,
      },
    ]);
    jest.mocked(resumeAgentChatStream).mockImplementationOnce(async ({ onEvent }) => {
      onEvent({
        type: "state",
        sessionId: "session-loaded",
        messages: [
          { id: "u1", role: "user", content: "继续分析" },
          { id: "a1", role: "assistant", content: "已有" },
        ],
        isStreaming: true,
        runStatus: "running",
      });
      onEvent({
        type: "token",
        sessionId: "session-loaded",
        content: "输出",
      });
      onEvent({
        type: "done",
        sessionId: "session-loaded",
      });
    });

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));
    await waitFor(() => expect(result.current.isStreaming).toBe(false));

    expect(result.current.messages).toEqual([
      expect.objectContaining({ id: "u1", role: "user", content: "继续分析" }),
      expect.objectContaining({ id: "a1", role: "assistant", content: "已有输出" }),
    ]);
  });

  it("applies question responses to the message that owns the request", async () => {
    listChatSessions.mockResolvedValue([
      {
        id: "session-streaming",
        title: "运行中",
        createdAt: 1,
        updatedAt: 2,
        isStreaming: true,
      },
    ]);
    jest.mocked(resumeAgentChatStream).mockImplementationOnce(async ({ onEvent }) => {
      onEvent({
        type: "state",
        sessionId: "session-loaded",
        messages: [
          { id: "u1", role: "user", content: "继续分析" },
          {
            id: "a1",
            role: "assistant",
            content: "需要确认",
            questions: [
              {
                requestId: "q-1",
                sessionId: "session-loaded",
                questions: [
                  {
                    header: "范围",
                    question: "选择范围",
                    options: [],
                    custom: true,
                  },
                ],
                createdAt: 123,
                status: "pending",
              },
            ],
          },
          { id: "a2", role: "assistant", content: "后续消息" },
        ],
        isStreaming: true,
        runStatus: "running",
      });
      onEvent({
        type: "question_response",
        sessionId: "session-loaded",
        requestId: "q-1",
        answers: [["城区"]],
        rejected: false,
      });
    });

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    expect(result.current.messages[1].questions?.[0]).toEqual(
      expect.objectContaining({
        requestId: "q-1",
        status: "answered",
        answers: [["城区"]],
      }),
    );
    expect(result.current.messages[2].questions).toBeUndefined();
  });

  it("deduplicates question requests across assistant messages", async () => {
    listChatSessions.mockResolvedValue([
      {
        id: "session-streaming",
        title: "运行中",
        createdAt: 1,
        updatedAt: 2,
        isStreaming: true,
      },
    ]);
    jest.mocked(resumeAgentChatStream).mockImplementationOnce(async ({ onEvent }) => {
      onEvent({
        type: "state",
        sessionId: "session-loaded",
        messages: [
          { id: "u1", role: "user", content: "继续分析" },
          {
            id: "a1",
            role: "assistant",
            content: "需要确认",
            questions: [
              {
                requestId: "question-1",
                sessionId: "session-loaded",
                questions: [
                  {
                    header: "测试问题",
                    question: "你觉得这个 question 工具好用吗？",
                    options: [
                      {
                        label: "非常好用",
                        description: "交互清晰，选项方便",
                      },
                    ],
                  },
                ],
                tool: {
                  messageID: "message-1",
                  callID: "call-1",
                },
                createdAt: 123,
                status: "pending",
              },
            ],
          },
          { id: "a2", role: "assistant", content: "后续消息" },
        ],
        isStreaming: true,
        runStatus: "running",
      });
      onEvent({
        type: "question_request",
        sessionId: "session-loaded",
        requestId: "call-1",
        questions: [
          {
            header: "测试问题",
            question: "你觉得这个 question 工具好用吗？",
            options: [
              {
                label: "非常好用",
                description: "交互清晰，选项方便",
              },
            ],
          },
        ],
        tool: {
          messageID: "message-1",
          callID: "call-1",
        },
        createdAt: 456,
      });
    });

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    const allQuestions = result.current.messages.flatMap(
      (message) => message.questions ?? [],
    );
    expect(allQuestions).toHaveLength(1);
    expect(result.current.messages[1].questions?.[0]).toEqual(
      expect.objectContaining({
        requestId: "question-1",
        tool: expect.objectContaining({ callID: "call-1" }),
      }),
    );
    expect(result.current.messages[2].questions).toBeUndefined();
  });

  it("keeps the actionable question request id when a tool-part duplicate arrives later", async () => {
    listChatSessions.mockResolvedValue([
      {
        id: "session-streaming",
        title: "运行中",
        createdAt: 1,
        updatedAt: 2,
        isStreaming: true,
      },
    ]);
    jest.mocked(resumeAgentChatStream).mockImplementationOnce(async ({ onEvent }) => {
      onEvent({
        type: "state",
        sessionId: "session-loaded",
        messages: [
          { id: "u1", role: "user", content: "继续分析" },
          {
            id: "a1",
            role: "assistant",
            content: "需要确认",
            questions: [
              {
                requestId: "question-1",
                sessionId: "session-loaded",
                questions: [
                  {
                    header: "测试问题",
                    question: "你觉得这个 question 工具好用吗？",
                    options: [
                      {
                        label: "非常好用",
                        description: "交互清晰，选项方便",
                      },
                    ],
                  },
                ],
                tool: {
                  messageID: "message-1",
                  callID: "call-1",
                },
                createdAt: 123,
                status: "pending",
              },
            ],
          },
        ],
        isStreaming: true,
        runStatus: "running",
      });
      onEvent({
        type: "question_request",
        sessionId: "session-loaded",
        requestId: "call-1",
        questions: [
          {
            header: "测试问题",
            question: "你觉得这个 question 工具好用吗？",
            options: [
              {
                label: "非常好用",
                description: "交互清晰，选项方便",
              },
            ],
          },
        ],
        tool: {
          messageID: "message-1",
          callID: "call-1",
        },
        createdAt: 456,
      });
    });

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    const allQuestions = result.current.messages.flatMap(
      (message) => message.questions ?? [],
    );
    expect(allQuestions).toHaveLength(1);
    expect(allQuestions[0]).toEqual(
      expect.objectContaining({
        requestId: "question-1",
        tool: expect.objectContaining({ callID: "call-1" }),
      }),
    );
  });

  it("deduplicates persisted duplicate questions from state events", async () => {
    listChatSessions.mockResolvedValue([
      {
        id: "session-streaming",
        title: "运行中",
        createdAt: 1,
        updatedAt: 2,
        isStreaming: true,
      },
    ]);
    const duplicateQuestion = {
      sessionId: "session-loaded",
      questions: [
        {
          header: "测试问题",
          question: "你觉得这个 question 工具好用吗？",
          options: [
            {
              label: "非常好用",
              description: "交互清晰，选项方便",
            },
          ],
        },
      ],
      tool: {
        messageID: "message-1",
        callID: "call-1",
      },
      createdAt: 123,
      status: "pending" as const,
    };
    jest.mocked(resumeAgentChatStream).mockImplementationOnce(async ({ onEvent }) => {
      onEvent({
        type: "state",
        sessionId: "session-loaded",
        messages: [
          { id: "u1", role: "user", content: "继续分析" },
          {
            id: "a1",
            role: "assistant",
            content: "需要确认",
            questions: [{ ...duplicateQuestion, requestId: "question-1" }],
          },
          {
            id: "a2",
            role: "assistant",
            content: "后续消息",
            questions: [{ ...duplicateQuestion, requestId: "call-1" }],
          },
        ],
        isStreaming: true,
        runStatus: "running",
      });
    });

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isHydrating).toBe(false));

    expect(
      result.current.messages.flatMap((message) => message.questions ?? []),
    ).toHaveLength(1);
    expect(result.current.messages[1].questions).toHaveLength(1);
    expect(result.current.messages[2].questions).toBeUndefined();
  });

  it("aborts a resumed streaming session through the backend abort endpoint", async () => {
    listChatSessions.mockResolvedValue([
      {
        id: "session-streaming",
        title: "运行中",
        createdAt: 1,
        updatedAt: 2,
        isStreaming: true,
      },
    ]);
    jest.mocked(resumeAgentChatStream).mockImplementationOnce(async () => {
      await new Promise<void>(() => undefined);
    });

    const { result } = renderHook(() =>
      useAgentChatSession({
        projectId: "project-1",
        onToolCall: jest.fn(),
      }),
    );

    await waitFor(() => expect(result.current.isStreaming).toBe(true));

    act(() => {
      result.current.abort();
    });

    expect(abortAgentChat).toHaveBeenCalledWith("session-loaded");
  });

});
});
