/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import React from "react";
import { render, screen, waitFor, fireEvent, act } from "@testing-library/react";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { LinkAutocomplete } from "../LinkAutocomplete";
import * as apiClient from "@/lib/api/client";

// Mock the API client
vi.mock("@/lib/api/client", () => ({
  apiRequest: vi.fn(),
}));

const mockApiRequest = apiClient.apiRequest as ReturnType<typeof vi.fn>;

describe("LinkAutocomplete", (): void => {
  let textareaRef: React.RefObject<HTMLTextAreaElement>;
  let onInsertMock: ReturnType<typeof vi.fn>;

  beforeEach((): void => {
    // Create a real textarea element
    const textarea = document.createElement("textarea");
    textarea.style.width = "500px";
    textarea.style.height = "300px";
    document.body.appendChild(textarea);

    textareaRef = { current: textarea };
    onInsertMock = vi.fn();

    // Reset mocks
    vi.clearAllMocks();
    mockApiRequest.mockResolvedValue({
      data: [],
      meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
    });
  });

  afterEach((): void => {
    // Clean up
    if (textareaRef.current) {
      document.body.removeChild(textareaRef.current);
    }
    vi.clearAllTimers();
  });

  it("should not show dropdown initially", (): void => {
    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    expect(screen.queryByText(/Start typing to search/)).not.toBeInTheDocument();
  });

  it("should show dropdown when typing [[", async (): Promise<void> => {
    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[ by setting value and triggering input event
    act(() => {
      textarea.value = "[[";
      textarea.setSelectionRange(2, 2);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.getByText(/Start typing to search/)).toBeInTheDocument();
    });
  });

  it("should pass an AbortSignal to apiRequest for cancellation", async (): Promise<void> => {
    vi.useFakeTimers();

    mockApiRequest.mockResolvedValue({
      data: [],
      meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
    });

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[abc
    act(() => {
      textarea.value = "[[abc";
      textarea.setSelectionRange(5, 5);
      fireEvent.input(textarea);
    });

    // Advance past debounce to fire the search
    await act(async () => {
      await vi.advanceTimersByTimeAsync(300);
    });

    // Verify apiRequest was called with a signal
    expect(mockApiRequest).toHaveBeenCalledTimes(1);
    const callArgs = mockApiRequest.mock.calls[0] as [
      string,
      { method: string; signal: AbortSignal },
    ];
    expect(callArgs[1]).toHaveProperty("signal");
    expect(callArgs[1].signal).toBeInstanceOf(AbortSignal);

    vi.useRealTimers();
  });

  it("should abort previous in-flight request when a new search fires", async (): Promise<void> => {
    vi.useFakeTimers();

    mockApiRequest.mockResolvedValue({
      data: [],
      meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
    });

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // First search: type [[foo
    act(() => {
      textarea.value = "[[foo";
      textarea.setSelectionRange(5, 5);
      fireEvent.input(textarea);
    });

    // Advance past debounce to fire the first search
    await act(async () => {
      await vi.advanceTimersByTimeAsync(300);
    });

    expect(mockApiRequest).toHaveBeenCalledTimes(1);
    const firstCallArgs = mockApiRequest.mock.calls[0] as [
      string,
      { method: string; signal: AbortSignal },
    ];
    const firstSignal = firstCallArgs[1].signal;
    expect(firstSignal.aborted).toBe(false);

    // Second search: type [[foobar (user continues typing)
    act(() => {
      textarea.value = "[[foobar";
      textarea.setSelectionRange(8, 8);
      fireEvent.input(textarea);
    });

    // The first signal should be aborted immediately when debouncedSearch fires again
    // (abort happens before the timeout, in the debounce function itself)
    expect(firstSignal.aborted).toBe(true);

    // Advance past debounce to fire the second search
    await act(async () => {
      await vi.advanceTimersByTimeAsync(300);
    });

    expect(mockApiRequest).toHaveBeenCalledTimes(2);
    const secondCallArgs = mockApiRequest.mock.calls[1] as [
      string,
      { method: string; signal: AbortSignal },
    ];
    const secondSignal = secondCallArgs[1].signal;
    expect(secondSignal.aborted).toBe(false);

    vi.useRealTimers();
  });

  it("should abort in-flight request on unmount", async (): Promise<void> => {
    vi.useFakeTimers();

    mockApiRequest.mockResolvedValue({
      data: [],
      meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
    });

    const { unmount } = render(
      <LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />
    );

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Trigger a search
    act(() => {
      textarea.value = "[[test";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    // Advance past debounce
    await act(async () => {
      await vi.advanceTimersByTimeAsync(300);
    });

    expect(mockApiRequest).toHaveBeenCalledTimes(1);
    const callArgs = mockApiRequest.mock.calls[0] as [
      string,
      { method: string; signal: AbortSignal },
    ];
    const signal = callArgs[1].signal;
    expect(signal.aborted).toBe(false);

    // Unmount the component - should abort in-flight request
    unmount();

    expect(signal.aborted).toBe(true);

    vi.useRealTimers();
  });

  it("should show error message when search fails", async (): Promise<void> => {
    vi.useFakeTimers();

    mockApiRequest.mockRejectedValue(new Error("Network error"));

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[fail
    act(() => {
      textarea.value = "[[fail";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    // Advance past debounce to fire the search
    await act(async () => {
      await vi.advanceTimersByTimeAsync(300);
    });

    // Allow microtasks (promise rejection handler) to settle
    await act(async () => {
      await vi.advanceTimersByTimeAsync(0);
    });

    // Should show PDA-friendly error message instead of "No entries found"
    expect(screen.getByText("Search unavailable — please try again")).toBeInTheDocument();

    // Verify "No entries found" is NOT shown (error takes precedence)
    expect(screen.queryByText("No entries found")).not.toBeInTheDocument();

    vi.useRealTimers();
  });

  it("should clear error message on successful search", async (): Promise<void> => {
    vi.useFakeTimers();

    // First search fails
    mockApiRequest.mockRejectedValueOnce(new Error("Network error"));

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Trigger failing search
    act(() => {
      textarea.value = "[[fail";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    await act(async () => {
      await vi.advanceTimersByTimeAsync(300);
    });

    // Allow microtasks (promise rejection handler) to settle
    await act(async () => {
      await vi.advanceTimersByTimeAsync(0);
    });

    expect(screen.getByText("Search unavailable — please try again")).toBeInTheDocument();

    // Second search succeeds
    mockApiRequest.mockResolvedValueOnce({
      data: [
        {
          id: "1",
          slug: "test-entry",
          title: "Test Entry",
          summary: "A test entry",
          workspaceId: "workspace-1",
          content: "Content",
          contentHtml: "<p>Content</p>",
          status: "PUBLISHED" as const,
          visibility: "PUBLIC" as const,
          createdBy: "user-1",
          updatedBy: "user-1",
          createdAt: new Date(),
          updatedAt: new Date(),
          tags: [],
        },
      ],
      meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
    });

    // Trigger successful search
    act(() => {
      textarea.value = "[[success";
      textarea.setSelectionRange(9, 9);
      fireEvent.input(textarea);
    });

    await act(async () => {
      await vi.advanceTimersByTimeAsync(300);
    });

    // Allow microtasks (promise resolution handler) to settle
    await act(async () => {
      await vi.advanceTimersByTimeAsync(0);
    });

    // Error message should be gone, results should show
    expect(screen.queryByText("Search unavailable — please try again")).not.toBeInTheDocument();
    expect(screen.getByText("Test Entry")).toBeInTheDocument();

    vi.useRealTimers();
  });

  it("should not show error for aborted requests", async (): Promise<void> => {
    vi.useFakeTimers();

    // Make the API reject with an AbortError
    const abortError = new DOMException("The operation was aborted.", "AbortError");
    mockApiRequest.mockRejectedValue(abortError);

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[abc
    act(() => {
      textarea.value = "[[abc";
      textarea.setSelectionRange(5, 5);
      fireEvent.input(textarea);
    });

    await act(async () => {
      await vi.advanceTimersByTimeAsync(300);
    });

    // Should NOT show error message for aborted requests
    // Allow a tick for the catch to process
    await act(async () => {
      await vi.advanceTimersByTimeAsync(0);
    });

    expect(screen.queryByText("Search unavailable — please try again")).not.toBeInTheDocument();

    vi.useRealTimers();
  });

  it("should perform debounced search when typing query", async (): Promise<void> => {
    const mockResults = {
      data: [
        {
          id: "1",
          slug: "test-entry",
          title: "Test Entry",
          summary: "A test entry",
          workspaceId: "workspace-1",
          content: "Content",
          contentHtml: "<p>Content</p>",
          status: "PUBLISHED" as const,
          visibility: "PUBLIC" as const,
          createdBy: "user-1",
          updatedBy: "user-1",
          createdAt: new Date(),
          updatedAt: new Date(),
          tags: [],
        },
      ],
      meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
    };

    mockApiRequest.mockResolvedValue(mockResults);

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[test
    act(() => {
      textarea.value = "[[test";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    // Should not call API immediately
    expect(mockApiRequest).not.toHaveBeenCalled();

    await waitFor(() => {
      expect(mockApiRequest).toHaveBeenCalledWith(
        "/api/knowledge/search?q=test&limit=10",
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        expect.objectContaining({ method: "GET", signal: expect.any(AbortSignal) })
      );
    });

    await waitFor(() => {
      expect(screen.getByText("Test Entry")).toBeInTheDocument();
    });
  });

  it("should navigate results with arrow keys", async (): Promise<void> => {
    const mockResults = {
      data: [
        {
          id: "1",
          slug: "entry-one",
          title: "Entry One",
          summary: "First entry",
          workspaceId: "workspace-1",
          content: "Content",
          contentHtml: "<p>Content</p>",
          status: "PUBLISHED" as const,
          visibility: "PUBLIC" as const,
          createdBy: "user-1",
          updatedBy: "user-1",
          createdAt: new Date(),
          updatedAt: new Date(),
          tags: [],
        },
        {
          id: "2",
          slug: "entry-two",
          title: "Entry Two",
          summary: "Second entry",
          workspaceId: "workspace-1",
          content: "Content",
          contentHtml: "<p>Content</p>",
          status: "PUBLISHED" as const,
          visibility: "PUBLIC" as const,
          createdBy: "user-1",
          updatedBy: "user-1",
          createdAt: new Date(),
          updatedAt: new Date(),
          tags: [],
        },
      ],
      meta: { total: 2, page: 1, limit: 10, totalPages: 1 },
    };

    mockApiRequest.mockResolvedValue(mockResults);

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[test
    act(() => {
      textarea.value = "[[test";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.getByText("Entry One")).toBeInTheDocument();
    });

    // First item should be selected (highlighted)
    const firstItem = screen.getByText("Entry One").closest("li");
    expect(firstItem).toHaveClass("bg-blue-50");

    // Press ArrowDown
    fireEvent.keyDown(textarea, { key: "ArrowDown" });

    // Second item should now be selected
    await waitFor(() => {
      const secondItem = screen.getByText("Entry Two").closest("li");
      expect(secondItem).toHaveClass("bg-blue-50");
    });

    // Press ArrowUp
    fireEvent.keyDown(textarea, { key: "ArrowUp" });

    // First item should be selected again
    await waitFor(() => {
      const firstItem = screen.getByText("Entry One").closest("li");
      expect(firstItem).toHaveClass("bg-blue-50");
    });
  });

  it("should insert link on Enter key", async (): Promise<void> => {
    const mockResults = {
      data: [
        {
          id: "1",
          slug: "test-entry",
          title: "Test Entry",
          summary: "A test entry",
          workspaceId: "workspace-1",
          content: "Content",
          contentHtml: "<p>Content</p>",
          status: "PUBLISHED" as const,
          visibility: "PUBLIC" as const,
          createdBy: "user-1",
          updatedBy: "user-1",
          createdAt: new Date(),
          updatedAt: new Date(),
          tags: [],
        },
      ],
      meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
    };

    mockApiRequest.mockResolvedValue(mockResults);

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[test
    act(() => {
      textarea.value = "[[test";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.getByText("Test Entry")).toBeInTheDocument();
    });

    // Press Enter to select
    fireEvent.keyDown(textarea, { key: "Enter" });

    await waitFor(() => {
      expect(onInsertMock).toHaveBeenCalledWith("[[test-entry|Test Entry]]");
    });
  });

  it("should insert link on click", async (): Promise<void> => {
    const mockResults = {
      data: [
        {
          id: "1",
          slug: "test-entry",
          title: "Test Entry",
          summary: "A test entry",
          workspaceId: "workspace-1",
          content: "Content",
          contentHtml: "<p>Content</p>",
          status: "PUBLISHED" as const,
          visibility: "PUBLIC" as const,
          createdBy: "user-1",
          updatedBy: "user-1",
          createdAt: new Date(),
          updatedAt: new Date(),
          tags: [],
        },
      ],
      meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
    };

    mockApiRequest.mockResolvedValue(mockResults);

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[test
    act(() => {
      textarea.value = "[[test";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.getByText("Test Entry")).toBeInTheDocument();
    });

    // Click on the result
    fireEvent.click(screen.getByText("Test Entry"));

    await waitFor(() => {
      expect(onInsertMock).toHaveBeenCalledWith("[[test-entry|Test Entry]]");
    });
  });

  it("should close dropdown on Escape key", async (): Promise<void> => {
    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[test
    act(() => {
      textarea.value = "[[test";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.getByText("↑↓ Navigate • Enter Select • Esc Cancel")).toBeInTheDocument();
    });

    // Press Escape
    fireEvent.keyDown(textarea, { key: "Escape" });

    await waitFor(() => {
      expect(screen.queryByText("↑↓ Navigate • Enter Select • Esc Cancel")).not.toBeInTheDocument();
    });
  });

  it("should close dropdown when closing brackets are typed", async (): Promise<void> => {
    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[test
    act(() => {
      textarea.value = "[[test";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.getByText("↑↓ Navigate • Enter Select • Esc Cancel")).toBeInTheDocument();
    });

    // Type closing brackets
    act(() => {
      textarea.value = "[[test]]";
      textarea.setSelectionRange(8, 8);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.queryByText("↑↓ Navigate • Enter Select • Esc Cancel")).not.toBeInTheDocument();
    });
  });

  it("should show 'No entries found' when search returns no results", async (): Promise<void> => {
    mockApiRequest.mockResolvedValue({
      data: [],
      meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
    });

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[nonexistent
    act(() => {
      textarea.value = "[[nonexistent";
      textarea.setSelectionRange(13, 13);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.getByText("No entries found")).toBeInTheDocument();
    });
  });

  it("should show loading state while searching", async (): Promise<void> => {
    // Mock a slow API response
    let resolveSearch: (value: {
      data: unknown[];
      meta: { total: number; page: number; limit: number; totalPages: number };
    }) => void = () => undefined;
    const searchPromise = new Promise<{
      data: unknown[];
      meta: { total: number; page: number; limit: number; totalPages: number };
    }>((resolve) => {
      resolveSearch = resolve;
    });
    mockApiRequest.mockReturnValue(searchPromise);

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[test
    act(() => {
      textarea.value = "[[test";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.getByText("Searching...")).toBeInTheDocument();
    });

    // Resolve the search
    resolveSearch({
      data: [],
      meta: { total: 0, page: 1, limit: 10, totalPages: 0 },
    });

    await waitFor(() => {
      expect(screen.queryByText("Searching...")).not.toBeInTheDocument();
    });
  });

  it("should display summary preview for entries", async (): Promise<void> => {
    const mockResults = {
      data: [
        {
          id: "1",
          slug: "test-entry",
          title: "Test Entry",
          summary: "This is a helpful summary",
          workspaceId: "workspace-1",
          content: "Content",
          contentHtml: "<p>Content</p>",
          status: "PUBLISHED" as const,
          visibility: "PUBLIC" as const,
          createdBy: "user-1",
          updatedBy: "user-1",
          createdAt: new Date(),
          updatedAt: new Date(),
          tags: [],
        },
      ],
      meta: { total: 1, page: 1, limit: 10, totalPages: 1 },
    };

    mockApiRequest.mockResolvedValue(mockResults);

    render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

    const textarea = textareaRef.current;
    if (!textarea) throw new Error("Textarea not found");

    // Simulate typing [[test
    act(() => {
      textarea.value = "[[test";
      textarea.setSelectionRange(6, 6);
      fireEvent.input(textarea);
    });

    await waitFor(() => {
      expect(screen.getByText("This is a helpful summary")).toBeInTheDocument();
    });
  });
});
