import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { describe, expect, it, vi, beforeEach } from "vitest";
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
import { useNewConversationCommand } from "#/hooks/mutation/use-new-conversation-command";

const mockNavigate = vi.fn();

vi.mock("react-router", () => ({
  useNavigate: () => mockNavigate,
  useParams: () => ({ conversationId: "conv-123" }),
}));

vi.mock("react-i18next", () => ({
  useTranslation: () => ({
    t: (key: string) => key,
  }),
}));

const { mockToast } = vi.hoisted(() => {
  const mockToast = Object.assign(vi.fn(), {
    loading: vi.fn(),
    dismiss: vi.fn(),
  });
  return { mockToast };
});

vi.mock("react-hot-toast", () => ({
  default: mockToast,
}));

vi.mock("#/utils/custom-toast-handlers", () => ({
  displaySuccessToast: vi.fn(),
  displayErrorToast: vi.fn(),
  TOAST_OPTIONS: { position: "top-right" },
}));

const mockConversation = {
  id: "conv-123",
  sandbox_id: "sandbox-456",
  title: "Test Conversation",
  selected_repository: null,
  selected_branch: null,
  git_provider: null,
  conversation_version: "V1" as const,
};

vi.mock("#/hooks/query/use-active-conversation", () => ({
  useActiveConversation: () => ({
    data: mockConversation,
  }),
}));

function makeStartTask(overrides: Record<string, unknown> = {}) {
  return {
    id: "task-789",
    created_by_user_id: null,
    status: "READY",
    detail: null,
    app_conversation_id: "new-conv-999",
    sandbox_id: "sandbox-456",
    agent_server_url: "http://agent-server.local",
    request: {
      sandbox_id: null,
      initial_message: null,
      processors: [],
      llm_model: null,
      selected_repository: null,
      selected_branch: null,
      git_provider: null,
      suggested_task: null,
      title: null,
      trigger: null,
      pr_number: [],
      parent_conversation_id: null,
      agent_type: "default",
    },
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
    ...overrides,
  };
}

describe("useNewConversationCommand", () => {
  let queryClient: QueryClient;

  beforeEach(() => {
    vi.clearAllMocks();
    queryClient = new QueryClient({
      defaultOptions: { mutations: { retry: false } },
    });
    // Mock batchGetAppConversations to return V1 data with llm_model
    vi.spyOn(
      V1ConversationService,
      "batchGetAppConversations",
    ).mockResolvedValue([
      {
        id: "conv-123",
        title: "Test Conversation",
        sandbox_id: "sandbox-456",
        sandbox_status: "RUNNING",
        execution_status: "IDLE",
        conversation_url: null,
        session_api_key: null,
        selected_repository: null,
        selected_branch: null,
        git_provider: null,
        trigger: null,
        pr_number: [],
        llm_model: "gpt-4o",
        metrics: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
        sub_conversation_ids: [],
        public: false,
      } as never,
    ]);
  });

  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  it("calls createConversation with sandbox_id and navigates on success", async () => {
    const readyTask = makeStartTask();
    const createSpy = vi
      .spyOn(V1ConversationService, "createConversation")
      .mockResolvedValue(readyTask as never);
    const getStartTaskSpy = vi
      .spyOn(V1ConversationService, "getStartTask")
      .mockResolvedValue(readyTask as never);

    const { result } = renderHook(() => useNewConversationCommand(), { wrapper });

    await result.current.mutateAsync();

    await waitFor(() => {
      expect(createSpy).toHaveBeenCalledWith(
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined, // plugins
        "sandbox-456",
        "gpt-4o",
      );
      expect(getStartTaskSpy).toHaveBeenCalledWith("task-789");
      expect(mockNavigate).toHaveBeenCalledWith(
        "/conversations/new-conv-999",
      );
    });
  });

  it("polls getStartTask until status is READY", async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });

    const workingTask = makeStartTask({
      status: "WORKING",
      app_conversation_id: null,
    });
    const readyTask = makeStartTask({ status: "READY" });

    vi.spyOn(V1ConversationService, "createConversation").mockResolvedValue(
      workingTask as never,
    );
    const getStartTaskSpy = vi
      .spyOn(V1ConversationService, "getStartTask")
      .mockResolvedValueOnce(workingTask as never)
      .mockResolvedValueOnce(readyTask as never);

    const { result } = renderHook(() => useNewConversationCommand(), { wrapper });

    const mutatePromise = result.current.mutateAsync();

    await vi.advanceTimersByTimeAsync(2000);
    await mutatePromise;

    await waitFor(() => {
      expect(getStartTaskSpy).toHaveBeenCalledTimes(2);
      expect(mockNavigate).toHaveBeenCalledWith(
        "/conversations/new-conv-999",
      );
    });

    vi.useRealTimers();
  });

  it("throws when task status is ERROR", async () => {
    const errorTask = makeStartTask({
      status: "ERROR",
      detail: "Sandbox crashed",
      app_conversation_id: null,
    });

    vi.spyOn(V1ConversationService, "createConversation").mockResolvedValue(
      errorTask as never,
    );
    vi.spyOn(V1ConversationService, "getStartTask").mockResolvedValue(
      errorTask as never,
    );

    const { result } = renderHook(() => useNewConversationCommand(), { wrapper });

    await expect(result.current.mutateAsync()).rejects.toThrow(
      "Sandbox crashed",
    );
  });

  it("invalidates conversation list queries on success", async () => {
    const readyTask = makeStartTask();

    vi.spyOn(V1ConversationService, "createConversation").mockResolvedValue(
      readyTask as never,
    );
    vi.spyOn(V1ConversationService, "getStartTask").mockResolvedValue(
      readyTask as never,
    );

    const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");

    const { result } = renderHook(() => useNewConversationCommand(), { wrapper });

    await result.current.mutateAsync();

    await waitFor(() => {
      expect(invalidateSpy).toHaveBeenCalledWith({
        queryKey: ["user", "conversations"],
      });
      expect(invalidateSpy).toHaveBeenCalledWith({
        queryKey: ["v1-batch-get-app-conversations"],
      });
    });
  });

  it("creates a standalone conversation (not a sub-conversation) so it appears in the list", async () => {
    const readyTask = makeStartTask();
    const createSpy = vi
      .spyOn(V1ConversationService, "createConversation")
      .mockResolvedValue(readyTask as never);
    vi.spyOn(V1ConversationService, "getStartTask").mockResolvedValue(
      readyTask as never,
    );

    const { result } = renderHook(() => useNewConversationCommand(), { wrapper });

    await result.current.mutateAsync();

    await waitFor(() => {
      // parent_conversation_id should be undefined so the new conversation
      // is NOT a sub-conversation and will appear in the conversation list.
      expect(createSpy).toHaveBeenCalledWith(
        undefined, // selectedRepository (null from mock)
        undefined, // git_provider (null from mock)
        undefined, // initialUserMsg
        undefined, // selected_branch (null from mock)
        undefined, // conversationInstructions
        undefined, // suggestedTask
        undefined, // trigger
        undefined, // parent_conversation_id is NOT set
        undefined, // agent_type
        undefined, // plugins
        "sandbox-456", // sandbox_id IS set to reuse the sandbox
        "gpt-4o", // llm_model IS inherited from the original conversation
      );
    });
  });

  it("does not pass the ACP display label as an LLM model", async () => {
    // ACP conversations store a human-readable label in llm_model (e.g. "ACP: claude-agent-acp")
    // rather than a real model identifier. The new-conversation command must not forward this
    // label as a model parameter — doing so would break LLM routing on the new conversation.
    // conversation_url is null here because the test only exercises the model-parameter path;
    // the ACP agent server URL is irrelevant to this assertion.
    vi.mocked(V1ConversationService.batchGetAppConversations).mockResolvedValue([
      {
        id: "conv-123",
        title: "ACP Conversation",
        sandbox_id: "sandbox-456",
        sandbox_status: "RUNNING",
        execution_status: "IDLE",
        conversation_url: null,
        session_api_key: null,
        selected_repository: null,
        selected_branch: null,
        git_provider: null,
        trigger: null,
        pr_number: [],
        agent_kind: "acp",
        llm_model: "ACP: claude-agent-acp",
        metrics: null,
        created_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
        sub_conversation_ids: [],
        public: false,
      } as never,
    ]);

    const readyTask = makeStartTask();
    const createSpy = vi
      .spyOn(V1ConversationService, "createConversation")
      .mockResolvedValue(readyTask as never);
    vi.spyOn(V1ConversationService, "getStartTask").mockResolvedValue(
      readyTask as never,
    );

    const { result } = renderHook(() => useNewConversationCommand(), { wrapper });

    await result.current.mutateAsync();

    await waitFor(() => {
      expect(createSpy).toHaveBeenCalledWith(
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        "sandbox-456",
        undefined,
      );
    });
  });

  it("shows a loading toast immediately and dismisses it on success", async () => {
    const readyTask = makeStartTask();

    vi.spyOn(V1ConversationService, "createConversation").mockResolvedValue(
      readyTask as never,
    );
    vi.spyOn(V1ConversationService, "getStartTask").mockResolvedValue(
      readyTask as never,
    );

    const { result } = renderHook(() => useNewConversationCommand(), { wrapper });

    await result.current.mutateAsync();

    await waitFor(() => {
      expect(mockToast.loading).toHaveBeenCalledWith(
        "CONVERSATION$CLEARING",
        expect.objectContaining({ id: "clear-conversation" }),
      );
      expect(mockToast.dismiss).toHaveBeenCalledWith("clear-conversation");
    });
  });
});
