import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

// Mock ResizeObserver which is used by Radix UI components
global.ResizeObserver = vi.fn().mockImplementation(() => ({
  observe: vi.fn(),
  unobserve: vi.fn(),
  disconnect: vi.fn(),
}));

const { mockRouterPush, mockDeleteMutate, mockUseConversations } = vi.hoisted(
  () => ({
    mockRouterPush: vi.fn(),
    mockDeleteMutate: vi.fn(),
    mockUseConversations: vi.fn(),
  }),
);

vi.mock("next/navigation");

vi.mock("@uidotdev/usehooks", () => ({
  useDebounce: (value: string) => value,
}));

vi.mock("@/lib/hooks/use-platform", () => ({
  usePlatform: () => ({ modKey: "⌘", altKey: "⌥", isMac: true }),
}));

vi.mock("@/lib/auth/auth.hook", () => ({
  useIsAuthenticated: () => true,
}));

vi.mock("@/lib/auth/auth.query");

vi.mock("@/lib/config/config.query");

vi.mock("@/lib/chat/chat-utils", () => ({
  getConversationDisplayTitle: (title: string | null) =>
    title ?? "Untitled chat",
}));

vi.mock("@/lib/chat/chat.query", () => ({
  useConversations: mockUseConversations,
  useDeleteConversation: () => ({
    mutate: mockDeleteMutate,
  }),
  usePinConversation: () => ({
    mutate: vi.fn(),
  }),
}));

// Store the onValueChange callback so tests can control selectedValue
let capturedOnValueChange: ((value: string) => void) | null = null;

vi.mock("@/components/ui/command", () => ({
  CommandDialog: ({
    children,
    open,
    onValueChange,
  }: {
    children: React.ReactNode;
    open: boolean;
    onValueChange?: (value: string) => void;
  }) => {
    // Capture the callback so tests can simulate selection
    capturedOnValueChange = onValueChange ?? null;
    return open ? <div data-testid="command-dialog">{children}</div> : null;
  },
  CommandInput: ({
    value,
    onValueChange,
    placeholder,
  }: {
    value: string;
    onValueChange: (v: string) => void;
    placeholder: string;
  }) => (
    <input
      data-testid="command-input"
      placeholder={placeholder}
      value={value}
      onChange={(e) => onValueChange(e.target.value)}
    />
  ),
  CommandList: ({ children }: { children: React.ReactNode }) => (
    <div>{children}</div>
  ),
  CommandEmpty: ({ children }: { children: React.ReactNode }) => (
    <div>{children}</div>
  ),
  CommandGroup: ({
    children,
    heading,
  }: {
    children: React.ReactNode;
    heading?: string;
  }) => (
    <div>
      {heading && <div>{heading}</div>}
      {children}
    </div>
  ),
  CommandItem: ({
    children,
    onSelect,
    value,
  }: {
    children: React.ReactNode;
    onSelect: () => void;
    value: string;
  }) => (
    <button type="button" data-testid={`cmd-item-${value}`} onClick={onSelect}>
      {children}
    </button>
  ),
  CommandSeparator: () => <hr />,
}));

vi.mock("@/components/ui/badge", () => ({
  Badge: ({ children }: { children: React.ReactNode }) => (
    <span>{children}</span>
  ),
}));

import { usePathname, useRouter } from "next/navigation";
// Import component after mocks
import { act } from "react";
import { useHasPermissions } from "@/lib/auth/auth.query";
import { useFeature } from "@/lib/config/config.query";
import { ConversationSearchPalette } from "./conversation-search-palette";

describe("ConversationSearchPalette", () => {
  const defaultProps = {
    open: true,
    onOpenChange: vi.fn(),
  };

  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(useRouter).mockReturnValue({
      push: mockRouterPush,
    } as unknown as ReturnType<typeof useRouter>);
    vi.mocked(useHasPermissions).mockReturnValue({
      data: true,
      isPending: false,
      isLoading: false,
    } as ReturnType<typeof useHasPermissions>);
    vi.mocked(useFeature).mockReturnValue(false);
    vi.mocked(usePathname).mockReturnValue("/chat");
    mockUseConversations.mockReturnValue({
      data: [
        {
          id: "conv-1",
          title: "First conversation",
          updatedAt: new Date().toISOString(),
          messages: [],
        },
        {
          id: "conv-2",
          title: "Second conversation",
          updatedAt: new Date().toISOString(),
          messages: [],
        },
      ],
      isLoading: false,
      isFetching: false,
    });
    capturedOnValueChange = null;
  });

  it("only enables conversation fetching while open", () => {
    const { rerender } = render(
      <ConversationSearchPalette {...defaultProps} open={false} />,
    );

    expect(mockUseConversations).toHaveBeenLastCalledWith(
      expect.objectContaining({ enabled: false }),
    );

    rerender(<ConversationSearchPalette {...defaultProps} open={true} />);

    expect(mockUseConversations).toHaveBeenLastCalledWith(
      expect.objectContaining({ enabled: true }),
    );
  });

  it("renders conversations when open", () => {
    render(<ConversationSearchPalette {...defaultProps} />);

    expect(screen.getByText("First conversation")).toBeInTheDocument();
    expect(screen.getByText("Second conversation")).toBeInTheDocument();
  });

  it("routes Connect to the classic connection page when beta is off", () => {
    // The Pages nav group (incl. Connect) renders when there are no conversations.
    mockUseConversations.mockReturnValue({
      data: [],
      isLoading: false,
      isFetching: false,
    });
    vi.mocked(useFeature).mockReturnValue(false);
    render(<ConversationSearchPalette {...defaultProps} />);

    fireEvent.click(screen.getByText("Connect"));

    expect(mockRouterPush).toHaveBeenCalledWith("/connection");
  });

  it("routes Connect to the new connection page when ARCHESTRA_BETA is on", () => {
    mockUseConversations.mockReturnValue({
      data: [],
      isLoading: false,
      isFetching: false,
    });
    vi.mocked(useFeature).mockReturnValue(true);
    render(<ConversationSearchPalette {...defaultProps} />);

    fireEvent.click(screen.getByText("Connect"));

    expect(mockRouterPush).toHaveBeenCalledWith("/connection_beta");
  });

  it("routes MCP Registry to the beta registry when ARCHESTRA_BETA is on", () => {
    mockUseConversations.mockReturnValue({
      data: [],
      isLoading: false,
      isFetching: false,
    });
    vi.mocked(useFeature).mockReturnValue(true);
    render(<ConversationSearchPalette {...defaultProps} />);

    fireEvent.click(screen.getByText("MCP Registry"));

    expect(mockRouterPush).toHaveBeenCalledWith("/mcp/registry/beta");
  });

  it("does not show the recent chats empty state in the full search palette", () => {
    mockUseConversations.mockReturnValue({
      data: [],
      isLoading: false,
      isFetching: false,
    });

    render(<ConversationSearchPalette {...defaultProps} />);

    expect(screen.getByText("New chat")).toBeInTheDocument();
    expect(screen.getByText("Pages")).toBeInTheDocument();
    expect(screen.queryByText("No recent chats")).not.toBeInTheDocument();
  });

  it("shows the recent chats empty state in recent chats view", () => {
    mockUseConversations.mockReturnValue({
      data: [],
      isLoading: false,
      isFetching: false,
    });

    render(<ConversationSearchPalette {...defaultProps} recentChatsView />);

    expect(screen.getByText("No recent chats")).toBeInTheDocument();
  });

  it("redirects to /chat when deleting the currently viewed conversation", () => {
    vi.mocked(usePathname).mockReturnValue("/chat/conv-1");

    render(<ConversationSearchPalette {...defaultProps} />);

    // Simulate selecting conv-1 via the captured onValueChange
    act(() => {
      capturedOnValueChange?.("conv-conv-1");
    });

    // Press 'd' once to enter pending deletion state
    fireEvent.keyDown(window, { key: "d", code: "KeyD" });

    // Press 'd' again to confirm deletion
    fireEvent.keyDown(window, { key: "d", code: "KeyD" });

    // Should have called deleteMutation.mutate with the conversation ID
    expect(mockDeleteMutate).toHaveBeenCalledWith(
      "conv-1",
      expect.objectContaining({ onSettled: expect.any(Function) }),
    );

    // Should redirect to /chat since the deleted conversation is currently open
    expect(mockRouterPush).toHaveBeenCalledWith("/chat");
  });

  it("does not redirect when deleting a conversation that is not currently viewed", () => {
    vi.mocked(usePathname).mockReturnValue("/chat/conv-2");

    render(<ConversationSearchPalette {...defaultProps} />);

    // Simulate selecting conv-1 via the captured onValueChange
    act(() => {
      capturedOnValueChange?.("conv-conv-1");
    });

    // Press 'd' once to enter pending deletion state
    fireEvent.keyDown(window, { key: "d", code: "KeyD" });

    // Press 'd' again to confirm deletion
    fireEvent.keyDown(window, { key: "d", code: "KeyD" });

    // Should have called deleteMutation.mutate
    expect(mockDeleteMutate).toHaveBeenCalledWith(
      "conv-1",
      expect.objectContaining({ onSettled: expect.any(Function) }),
    );

    // Should NOT redirect since the deleted conversation is not the one currently open
    expect(mockRouterPush).not.toHaveBeenCalled();
  });

  it("does not redirect when deleting a conversation and no conversation is open", () => {
    render(<ConversationSearchPalette {...defaultProps} />);

    // Simulate selecting conv-1
    act(() => {
      capturedOnValueChange?.("conv-conv-1");
    });

    // Press 'd' twice to delete
    fireEvent.keyDown(window, { key: "d", code: "KeyD" });
    fireEvent.keyDown(window, { key: "d", code: "KeyD" });

    expect(mockDeleteMutate).toHaveBeenCalledWith(
      "conv-1",
      expect.objectContaining({ onSettled: expect.any(Function) }),
    );
    expect(mockRouterPush).not.toHaveBeenCalled();
  });

  it("prevents rapid double-deletion of the same conversation", () => {
    render(<ConversationSearchPalette {...defaultProps} />);

    // Select conv-1
    act(() => {
      capturedOnValueChange?.("conv-conv-1");
    });

    // Press 'd' once → pending state
    fireEvent.keyDown(window, { key: "d", code: "KeyD" });
    // Press 'd' again → confirms deletion
    fireEvent.keyDown(window, { key: "d", code: "KeyD" });

    expect(mockDeleteMutate).toHaveBeenCalledTimes(1);

    // Rapid third 'd' press in the same frame — should be ignored
    // because the ref guard prevents double-deletion before React re-renders
    fireEvent.keyDown(window, { key: "d", code: "KeyD" });

    expect(mockDeleteMutate).toHaveBeenCalledTimes(1);
  });

  it("navigates to conversation when selecting it", () => {
    render(<ConversationSearchPalette {...defaultProps} />);

    // Click a conversation item
    fireEvent.click(screen.getByTestId("cmd-item-conv-conv-1"));

    expect(mockRouterPush).toHaveBeenCalledWith("/chat/conv-1");
  });

  it("navigates to /chat when selecting new chat", () => {
    render(<ConversationSearchPalette {...defaultProps} />);

    fireEvent.click(screen.getByTestId("cmd-item-new-chat"));

    expect(mockRouterPush).toHaveBeenCalledWith("/chat");
  });

  it("navigates to LLM credentials when selecting Credentials", () => {
    render(<ConversationSearchPalette {...defaultProps} />);

    fireEvent.click(screen.getByRole("button", { name: "Credentials" }));

    expect(mockRouterPush).toHaveBeenCalledWith(
      "/llm/credentials/virtual-keys",
    );
  });
});
