/**
 * @file Chat.test.tsx
 * @description Tests for Chat component error handling in imperative handle methods
 */

import { createRef } from "react";
import { render, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, beforeEach, vi, afterEach, type MockedFunction } from "vitest";
import { Chat, type ChatRef } from "./Chat";
import * as useChatModule from "@/hooks/useChat";
import * as useWebSocketModule from "@/hooks/useWebSocket";
import * as authModule from "@/lib/auth/auth-context";
import * as orchestratorModule from "@/hooks/useOrchestratorCommands";

// Mock scrollIntoView (not available in JSDOM)
Element.prototype.scrollIntoView = vi.fn();

// Mock dependencies
vi.mock("@/lib/auth/auth-context", () => ({
  useAuth: vi.fn(),
}));

vi.mock("@/hooks/useChat", () => ({
  useChat: vi.fn(),
}));

vi.mock("@/hooks/useWebSocket", () => ({
  useWebSocket: vi.fn(),
}));

vi.mock("./MessageList", () => ({
  MessageList: (): React.ReactElement => <div data-testid="message-list" />,
}));

vi.mock("./ChatInput", () => ({
  ChatInput: ({
    onSend,
  }: {
    onSend: (content: string) => Promise<void>;
    disabled: boolean;
    inputRef: React.RefObject<HTMLTextAreaElement | null>;
  }): React.ReactElement => (
    <>
      <button data-testid="chat-input" onClick={(): void => void onSend("test message")}>
        Send
      </button>
      <button data-testid="chat-input-command" onClick={(): void => void onSend("/status")}>
        Send Command
      </button>
    </>
  ),
  DEFAULT_TEMPERATURE: 0.7,
  DEFAULT_MAX_TOKENS: 4096,
  DEFAULT_MODEL: "llama3.2",
  AVAILABLE_MODELS: [
    { id: "llama3.2", label: "Llama 3.2" },
    { id: "claude-3.5-sonnet", label: "Claude 3.5 Sonnet" },
    { id: "gpt-4o", label: "GPT-4o" },
    { id: "deepseek-r1", label: "DeepSeek R1" },
  ],
}));

vi.mock("@/hooks/useOrchestratorCommands", () => ({
  useOrchestratorCommands: vi.fn(),
}));

const mockUseAuth = authModule.useAuth as MockedFunction<typeof authModule.useAuth>;
const mockUseChat = useChatModule.useChat as MockedFunction<typeof useChatModule.useChat>;
const mockUseWebSocket = useWebSocketModule.useWebSocket as MockedFunction<
  typeof useWebSocketModule.useWebSocket
>;
const mockUseOrchestratorCommands = orchestratorModule.useOrchestratorCommands as MockedFunction<
  typeof orchestratorModule.useOrchestratorCommands
>;

function createMockUseChatReturn(
  overrides: Partial<useChatModule.UseChatReturn> = {}
): useChatModule.UseChatReturn {
  return {
    messages: [
      {
        id: "welcome",
        role: "assistant",
        content: "Hello!",
        createdAt: new Date().toISOString(),
      },
    ],
    isLoading: false,
    isStreaming: false,
    error: null,
    conversationId: null,
    conversationTitle: null,
    sendMessage: vi.fn().mockResolvedValue(undefined),
    abortStream: vi.fn(),
    loadConversation: vi.fn().mockResolvedValue(undefined),
    startNewConversation: vi.fn(),
    setMessages: vi.fn(),
    clearError: vi.fn(),
    ...overrides,
  };
}

describe("Chat", () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    vi.clearAllMocks();
    consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);

    mockUseAuth.mockReturnValue({
      user: { id: "user-1", name: "Test User", email: "test@test.com" },
      isAuthenticated: true,
      isLoading: false,
      login: vi.fn(),
      logout: vi.fn(),
    } as unknown as ReturnType<typeof authModule.useAuth>);

    mockUseWebSocket.mockReturnValue({
      isConnected: true,
      socket: null,
      connectionError: null,
    });

    // Default: no commands intercepted
    mockUseOrchestratorCommands.mockReturnValue({
      isCommand: vi.fn().mockReturnValue(false),
      executeCommand: vi.fn().mockResolvedValue(null),
    });
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  describe("loadConversation via ref", () => {
    it("should delegate to useChat.loadConversation", async () => {
      const mockLoadConversation = vi.fn().mockResolvedValue(undefined);
      mockUseChat.mockReturnValue(
        createMockUseChatReturn({ loadConversation: mockLoadConversation })
      );

      const ref = createRef<ChatRef>();
      render(<Chat ref={ref} />);

      await ref.current?.loadConversation("conv-123");

      expect(mockLoadConversation).toHaveBeenCalledWith("conv-123");
    });

    it("should not re-throw when useChat.loadConversation handles errors internally", async () => {
      // useChat.loadConversation handles errors internally (sets error state, logs, calls onError)
      // and does NOT re-throw, so the imperative handle should resolve cleanly
      const mockLoadConversation = vi.fn().mockResolvedValue(undefined);
      mockUseChat.mockReturnValue(
        createMockUseChatReturn({ loadConversation: mockLoadConversation })
      );

      const ref = createRef<ChatRef>();
      render(<Chat ref={ref} />);

      // Should resolve without throwing
      await expect(ref.current?.loadConversation("conv-123")).resolves.toBeUndefined();
    });
  });

  describe("sendMessage delegation", () => {
    it("should delegate to useChat.sendMessage", async () => {
      const mockSendMessage = vi.fn().mockResolvedValue(undefined);
      mockUseChat.mockReturnValue(createMockUseChatReturn({ sendMessage: mockSendMessage }));

      const ref = createRef<ChatRef>();
      const { getByTestId } = render(<Chat ref={ref} />);

      const sendButton = getByTestId("chat-input");
      sendButton.click();

      await vi.waitFor(() => {
        expect(mockSendMessage).toHaveBeenCalledWith("test message");
      });
    });
  });

  describe("orchestrator command routing", () => {
    it("routes command messages through orchestrator instead of LLM", async () => {
      const mockSendMessage = vi.fn().mockResolvedValue(undefined);
      const mockSetMessages = vi.fn();
      const mockExecuteCommand = vi.fn().mockResolvedValue({
        id: "orch-123",
        role: "assistant" as const,
        content: "**Orchestrator Status**\n\n| Field | Value |\n|---|---|\n| Status | **Ready** |",
        createdAt: new Date().toISOString(),
      });

      mockUseChat.mockReturnValue(
        createMockUseChatReturn({
          sendMessage: mockSendMessage,
          setMessages: mockSetMessages,
        })
      );

      mockUseOrchestratorCommands.mockReturnValue({
        isCommand: (content: string) => content.trim().startsWith("/"),
        executeCommand: mockExecuteCommand,
      });

      const { getByTestId } = render(<Chat />);

      const commandButton = getByTestId("chat-input-command");
      fireEvent.click(commandButton);

      await waitFor(() => {
        // executeCommand should have been called with the slash command
        expect(mockExecuteCommand).toHaveBeenCalledWith("/status");
      });

      // sendMessage should NOT have been called
      expect(mockSendMessage).not.toHaveBeenCalled();

      // setMessages should have been called to add user and assistant messages
      await waitFor(() => {
        expect(mockSetMessages).toHaveBeenCalledTimes(2);
      });
    });

    it("does not call orchestrator for regular messages", async () => {
      const mockSendMessage = vi.fn().mockResolvedValue(undefined);
      const mockExecuteCommand = vi.fn().mockResolvedValue(null);

      mockUseChat.mockReturnValue(createMockUseChatReturn({ sendMessage: mockSendMessage }));

      mockUseOrchestratorCommands.mockReturnValue({
        isCommand: vi.fn().mockReturnValue(false),
        executeCommand: mockExecuteCommand,
      });

      const { getByTestId } = render(<Chat />);

      fireEvent.click(getByTestId("chat-input"));

      await waitFor(() => {
        expect(mockSendMessage).toHaveBeenCalledWith("test message");
      });

      expect(mockExecuteCommand).not.toHaveBeenCalled();
    });

    it("still adds user message to chat for commands", async () => {
      const mockSetMessages = vi.fn();
      const mockExecuteCommand = vi.fn().mockResolvedValue({
        id: "orch-456",
        role: "assistant" as const,
        content: "Help content",
        createdAt: new Date().toISOString(),
      });

      mockUseChat.mockReturnValue(createMockUseChatReturn({ setMessages: mockSetMessages }));

      mockUseOrchestratorCommands.mockReturnValue({
        isCommand: (content: string) => content.trim().startsWith("/"),
        executeCommand: mockExecuteCommand,
      });

      const { getByTestId } = render(<Chat />);

      fireEvent.click(getByTestId("chat-input-command"));

      await waitFor(() => {
        expect(mockSetMessages).toHaveBeenCalled();
      });

      // First setMessages call should add the user message
      const firstCall = mockSetMessages.mock.calls[0];
      if (!firstCall) throw new Error("Expected setMessages to have been called");
      const updater = firstCall[0] as (prev: useChatModule.Message[]) => useChatModule.Message[];
      const result = updater([]);
      expect(result).toHaveLength(1);
      expect(result[0]).toMatchObject({
        role: "user",
        content: "/status",
      });
    });
  });
});
