"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 actions", () => {
  it("tracks permission requests and submits replies", 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));

    await act(async () => {
      void result.current.sendPrompt("删除临时文件");
      await Promise.resolve();
    });

    act(() => {
      emitStreamEvent?.({
        type: "permission_request",
        sessionId: "session-1",
        requestId: "perm-1",
        permission: "bash",
        patterns: ["rm *"],
        target: "rm tmp.txt",
        always: ["rm *"],
        createdAt: 123,
      });
    });

    expect(result.current.messages.at(-1)?.permissions).toEqual([
      expect.objectContaining({
        requestId: "perm-1",
        sessionId: "session-1",
        status: "pending",
      }),
    ]);

    await act(async () => {
      await result.current.replyPermission("perm-1", "once");
    });

    expect(replyAgentPermission).toHaveBeenCalledWith("session-1", "perm-1", "once");
    expect(result.current.messages.at(-1)?.permissions?.[0]).toEqual(
      expect.objectContaining({
        requestId: "perm-1",
        status: "approved_once",
      }),
    );
  });

  it("finalizes running progress when aborting an active prompt", async () => {
    listChatSessions.mockResolvedValue([]);
    jest.mocked(streamAgentChat).mockImplementationOnce(
      ({ onEvent, signal }) =>
        new Promise<void>((_, reject) => {
          onEvent({
            type: "progress",
            sessionId: "session-1",
            id: "request-received",
            phase: "start",
            status: "running",
            title: "开始分析",
            startedAt: 1000,
          } satisfies StreamEvent);
          onEvent({
            type: "todo_update",
            sessionId: "session-1",
            todos: [
              {
                id: "todo-1",
                content: "分析水位",
                status: "in_progress",
              },
              {
                id: "todo-2",
                content: "生成建议",
                status: "pending",
              },
            ],
            createdAt: 1001,
          } satisfies StreamEvent);
          onEvent({
            type: "permission_request",
            sessionId: "session-1",
            requestId: "perm-abort",
            permission: "bash",
            patterns: ["npm test"],
            target: "npm test",
            always: ["npm test"],
            createdAt: 1002,
          } satisfies StreamEvent);
          onEvent({
            type: "question_request",
            sessionId: "session-1",
            requestId: "question-abort",
            questions: [
              {
                header: "范围",
                question: "请选择范围",
                options: [{ label: "城区", description: "中心城区" }],
              },
            ],
            createdAt: 1003,
          } satisfies StreamEvent);

          signal?.addEventListener("abort", () => {
            reject(new Error("aborted"));
          });
        }),
    );

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

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

    act(() => {
      void result.current.sendPrompt("测试中断");
    });

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

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

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

    expect(result.current.messages.at(-1)).toEqual(
      expect.objectContaining({
        role: "assistant",
        content: "⚠️ **请求已中断**",
        isError: true,
        progress: [
          expect.objectContaining({
            id: "request-received",
            status: "completed",
            durationMs: expect.any(Number),
            endedAt: expect.any(Number),
          }),
        ],
        todos: expect.objectContaining({
          todos: [
            expect.objectContaining({
              id: "todo-1",
              status: "cancelled",
              updatedAt: expect.any(Number),
            }),
            expect.objectContaining({
              id: "todo-2",
              status: "cancelled",
              updatedAt: expect.any(Number),
            }),
          ],
        }),
        permissions: [
          expect.objectContaining({
            requestId: "perm-abort",
            status: "aborted",
            repliedAt: expect.any(Number),
            error: undefined,
          }),
        ],
        questions: [
          expect.objectContaining({
            requestId: "question-abort",
            status: "rejected",
            repliedAt: expect.any(Number),
            error: undefined,
          }),
        ],
      }),
    );
    expect(abortAgentChat).toHaveBeenCalledWith("session-1");
  });

  it("ignores generated session titles after the title was edited manually", async () => {
    listChatSessions.mockResolvedValue([]);
    jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => {
      onEvent({
        type: "session_title",
        sessionId: "session-1",
        title: "自动标题",
      });
      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.switchSession("session-loaded");
    });

    await act(async () => {
      await result.current.renameSession("session-loaded", "手动标题");
    });

    await waitFor(() => expect(updateChatSessionTitle).toHaveBeenCalled());

    await act(async () => {
      await result.current.sendPrompt("帮我分析一下");
    });

    expect(result.current.sessionTitle).toBe("手动标题");
    expect(updateChatSessionTitle).not.toHaveBeenCalledWith(
      "session-loaded",
      "自动标题",
      expect.anything(),
    );
  });

  it("does not apply a late generated title to a newly created session", async () => {
    listChatSessions.mockResolvedValue([]);
    let emitStreamEvent: ((event: StreamEvent) => void) | undefined;
    let resolveStream: (() => void) | undefined;
    jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => {
      emitStreamEvent = onEvent;
      await new Promise<void>((resolve) => {
        resolveStream = resolve;
      });
    });

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

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

    await act(async () => {
      void result.current.sendPrompt("帮我分析一下");
      await Promise.resolve();
    });

    act(() => {
      emitStreamEvent?.({
        type: "done",
        sessionId: "old-session",
      });
    });

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

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

    expect(result.current.sessionTitle).toBe("新对话");

    await act(async () => {
      emitStreamEvent?.({
        type: "session_title",
        sessionId: "old-session",
        title: "旧请求标题",
      });
      resolveStream?.();
      await Promise.resolve();
    });

    expect(result.current.sessionTitle).toBe("新对话");
    expect(updateChatSessionTitle).toHaveBeenCalledWith(
      "old-session",
      "旧请求标题",
      { isTitleManuallyEdited: false },
    );
  });

  it("forks a copied conversation from an assistant message", async () => {
    listChatSessions.mockResolvedValue([]);

    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("第一轮");
    });

    const firstAssistantMessageId = result.current.messages[1]?.id ?? "";

    await act(async () => {
      await result.current.createBranch(firstAssistantMessageId);
    });

    expect(forkAgentChat).toHaveBeenCalledWith(undefined, 2);
    expect(result.current.activeSessionId).toBe("forked-session");
    expect(result.current.messages).toHaveLength(2);
    expect(result.current.messages[0]).toEqual(
      expect.objectContaining({
        role: "user",
        content: "第一轮",
      }),
    );
    expect(result.current.messages[1]).toEqual(
      expect.objectContaining({
        role: "assistant",
      }),
    );
    expect(streamAgentChat).toHaveBeenCalledTimes(1);
  });
});
});
