/**
 * @vitest-environment happy-dom
 */
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useProductSearch } from "./useProductSearch";
import type { SearchResultItem } from "./types";

// ─── Mocks ──────────────────────────────────────────────────────────────────

const mockUseQuery = vi.fn();
vi.mock("convex/react", () => ({
  useQuery: (...args: unknown[]) => mockUseQuery(...args),
}));

const mockRouterPush = vi.fn();
vi.mock("next/navigation", () => ({
  useRouter: () => ({ push: mockRouterPush }),
}));

vi.mock("../../../../../convex/_generated/api", () => ({
  api: { products: { searchTypeahead: "mock-searchTypeahead-ref" } },
}));

// Inline getProductUrl so we avoid resolving @repo/utils transitively
vi.mock("../shop/productMapper", () => ({
  getProductUrl: (p: { parentCategorySlug: string; childCategorySlug: string; slug: string }) =>
    `/shop/${p.parentCategorySlug}/${p.childCategorySlug}/${p.slug}`,
}));

// ─── Helpers ─────────────────────────────────────────────────────────────────

function makeItem(overrides: Partial<SearchResultItem> = {}): SearchResultItem {
  return {
    _id: "p1",
    name: "Dog Food",
    slug: "dog-food",
    parentCategorySlug: "dogs",
    childCategorySlug: "dog-food-cat",
    brand: undefined,
    imageUrl: undefined,
    imageAlt: undefined,
    minPrice: 500,
    compareAtPrice: undefined,
    averageRating: undefined,
    reviewCount: undefined,
    ...overrides,
  };
}

function fakeKey(key: string) {
  return { key, preventDefault: vi.fn() } as unknown as React.KeyboardEvent;
}

// ─── Tests ───────────────────────────────────────────────────────────────────

describe("useProductSearch", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    // Default: skip → undefined (not searching)
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return undefined; // loading
    });
  });

  afterEach(() => {
    act(() => vi.runAllTimers());
    vi.useRealTimers();
    vi.clearAllMocks();
  });

  // ── Initial state ──────────────────────────────────────────────────────────

  it("initialises with empty state and closed panel", () => {
    const { result } = renderHook(() => useProductSearch());
    expect(result.current.query).toBe("");
    expect(result.current.categorySlug).toBeUndefined();
    expect(result.current.isOpen).toBe(false);
    expect(result.current.selectedIndex).toBe(-1);
    expect(result.current.results).toEqual([]);
    expect(result.current.isLoading).toBe(false);
    expect(result.current.isEmpty).toBe(false);
    expect(result.current.showResults).toBe(false);
    expect(result.current.showMinCharsHint).toBe(false);
  });

  it("exposes inputRef and panelRef", () => {
    const { result } = renderHook(() => useProductSearch());
    expect(result.current.inputRef).toBeDefined();
    expect(result.current.panelRef).toBeDefined();
  });

  // ── setQuery ──────────────────────────────────────────────────────────────

  it("setQuery tracks the query value and opens the panel", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dogs"));
    expect(result.current.query).toBe("dogs");
    expect(result.current.isOpen).toBe(true);
  });

  it("setQuery with empty string closes the panel and resets selectedIndex", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dogs"));
    act(() => result.current.setSelectedIndex(2));
    act(() => result.current.setQuery(""));
    expect(result.current.query).toBe("");
    expect(result.current.isOpen).toBe(false);
    expect(result.current.selectedIndex).toBe(-1);
  });

  it("caps query at 100 characters", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("a".repeat(150)));
    expect(result.current.query).toHaveLength(100);
  });

  // ── showMinCharsHint ───────────────────────────────────────────────────────

  it("showMinCharsHint is true when panel open and 0 < query < 3 chars", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("do"));
    expect(result.current.showMinCharsHint).toBe(true);
    expect(result.current.showResults).toBe(false);
  });

  it("showMinCharsHint is false when query is empty", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery(""));
    expect(result.current.showMinCharsHint).toBe(false);
  });

  it("showMinCharsHint is false when query is 3+ chars", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog"));
    expect(result.current.showMinCharsHint).toBe(false);
  });

  // ── open / close ──────────────────────────────────────────────────────────

  it("open() opens the panel", () => {
    const { result } = renderHook(() => useProductSearch());
    expect(result.current.isOpen).toBe(false);
    act(() => result.current.open());
    expect(result.current.isOpen).toBe(true);
  });

  it("close() closes the panel and resets selectedIndex", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.open());
    act(() => result.current.setSelectedIndex(2));
    act(() => result.current.close());
    expect(result.current.isOpen).toBe(false);
    expect(result.current.selectedIndex).toBe(-1);
  });

  // ── Convex query integration (after debounce) ─────────────────────────────

  it("calls useQuery with skip before debounce fires", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog food"));
    // Timer has NOT advanced — debouncedQuery is still ""
    const lastCall = mockUseQuery.mock.calls.at(-1)!;
    expect(lastCall[1]).toBe("skip");
  });

  it("calls useQuery with real args after debounce fires", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog food"));
    act(() => vi.advanceTimersByTime(300));
    const lastCall = mockUseQuery.mock.calls.at(-1)!;
    expect(lastCall[0]).toBe("mock-searchTypeahead-ref");
    expect(lastCall[1]).toMatchObject({ query: "dog food", limit: 8 });
  });

  it("passes parentCategorySlug when categorySlug is set", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setCategorySlug("dogs"));
    act(() => result.current.setQuery("kibble"));
    act(() => vi.advanceTimersByTime(300));
    const lastCall = mockUseQuery.mock.calls.at(-1)!;
    expect(lastCall[1]).toMatchObject({ query: "kibble", parentCategorySlug: "dogs" });
  });

  it("uses skip when debounced query is shorter than 3 chars", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("ab"));
    act(() => vi.advanceTimersByTime(300));
    const lastCall = mockUseQuery.mock.calls.at(-1)!;
    expect(lastCall[1]).toBe("skip");
  });

  // ── isLoading / isEmpty / results ─────────────────────────────────────────

  it("isLoading is true when shouldSearch but rawResults is undefined", () => {
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return undefined; // loading in-flight
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog food"));
    act(() => vi.advanceTimersByTime(300));
    expect(result.current.isLoading).toBe(true);
  });

  it("isLoading is false when rawResults is an array", () => {
    const items = [makeItem()];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return items;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog food"));
    act(() => vi.advanceTimersByTime(300));
    expect(result.current.isLoading).toBe(false);
  });

  it("isEmpty is true when rawResults is an empty array", () => {
    const empty: SearchResultItem[] = [];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return empty;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog food"));
    act(() => vi.advanceTimersByTime(300));
    expect(result.current.isEmpty).toBe(true);
    expect(result.current.results).toEqual([]);
  });

  it("results is populated from rawResults", () => {
    const items = [makeItem({ _id: "p1", name: "Dog Food" })];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return items;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog food"));
    act(() => vi.advanceTimersByTime(300));
    expect(result.current.results).toHaveLength(1);
    expect(result.current.results[0].name).toBe("Dog Food");
  });

  // ── showResults ────────────────────────────────────────────────────────────

  it("showResults is true when panel is open and shouldSearch is true", () => {
    const empty: SearchResultItem[] = [];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return empty;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog food"));
    act(() => vi.advanceTimersByTime(300));
    expect(result.current.showResults).toBe(true);
  });

  it("showResults is false when panel is closed even if shouldSearch is true", () => {
    const empty: SearchResultItem[] = [];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return empty;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog food"));
    act(() => vi.advanceTimersByTime(300));
    act(() => result.current.close());
    expect(result.current.showResults).toBe(false);
  });

  // ── Keyboard navigation ────────────────────────────────────────────────────

  it("ArrowDown increments selectedIndex from -1", () => {
    const items = [makeItem({ _id: "p1" }), makeItem({ _id: "p2" })];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return items;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog"));
    act(() => vi.advanceTimersByTime(300));
    act(() => result.current.handleKeyDown(fakeKey("ArrowDown")));
    expect(result.current.selectedIndex).toBe(0);
    act(() => result.current.handleKeyDown(fakeKey("ArrowDown")));
    expect(result.current.selectedIndex).toBe(1);
  });

  it("ArrowDown does not exceed results.length - 1", () => {
    const items = [makeItem()];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return items;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog"));
    act(() => vi.advanceTimersByTime(300));
    act(() => result.current.handleKeyDown(fakeKey("ArrowDown")));
    act(() => result.current.handleKeyDown(fakeKey("ArrowDown")));
    expect(result.current.selectedIndex).toBe(0); // capped at 0
  });

  it("ArrowUp decrements selectedIndex, clamped at -1", () => {
    const items = [makeItem()];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return items;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog"));
    act(() => vi.advanceTimersByTime(300));
    act(() => result.current.handleKeyDown(fakeKey("ArrowDown")));
    expect(result.current.selectedIndex).toBe(0);
    act(() => result.current.handleKeyDown(fakeKey("ArrowUp")));
    expect(result.current.selectedIndex).toBe(-1);
    act(() => result.current.handleKeyDown(fakeKey("ArrowUp")));
    expect(result.current.selectedIndex).toBe(-1); // stays at -1
  });

  it("Enter navigates to the selected product page and closes the panel", () => {
    const item = makeItem({
      slug: "dog-food",
      parentCategorySlug: "dogs",
      childCategorySlug: "dog-food-cat",
    });
    // Stable reference — prevents useMemo seeing a new array every render,
    // which would reset selectedIndex via useEffect([results]) on each render.
    const stableResults = [item];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return stableResults;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog"));
    act(() => vi.advanceTimersByTime(300));
    act(() => result.current.handleKeyDown(fakeKey("ArrowDown")));
    act(() => result.current.handleKeyDown(fakeKey("Enter")));
    expect(mockRouterPush).toHaveBeenCalledWith("/shop/dogs/dog-food-cat/dog-food");
    expect(result.current.isOpen).toBe(false);
  });

  it("Enter does nothing when selectedIndex is -1", () => {
    const items = [makeItem()];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return items;
    });
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog"));
    act(() => vi.advanceTimersByTime(300));
    // selectedIndex is -1 (reset by results arriving)
    act(() => result.current.handleKeyDown(fakeKey("Enter")));
    expect(mockRouterPush).not.toHaveBeenCalled();
    expect(result.current.isOpen).toBe(true);
  });

  it("Escape closes the panel", () => {
    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dogs"));
    expect(result.current.isOpen).toBe(true);
    act(() => result.current.handleKeyDown(fakeKey("Escape")));
    expect(result.current.isOpen).toBe(false);
  });

  // ── selectedIndex reset ────────────────────────────────────────────────────

  it("selectedIndex resets to -1 when results change", () => {
    let currentResults: SearchResultItem[] = [];
    mockUseQuery.mockImplementation((_ref: unknown, args: unknown) => {
      if (args === "skip") return undefined;
      return currentResults;
    });

    const { result } = renderHook(() => useProductSearch());
    act(() => result.current.setQuery("dog"));
    act(() => vi.advanceTimersByTime(300));

    // Manually set selectedIndex
    act(() => result.current.setSelectedIndex(0));
    expect(result.current.selectedIndex).toBe(0);

    // New results arrive (new reference)
    currentResults = [makeItem({ _id: "p-new" })];
    // Force re-render by updating query so debounced query changes
    act(() => result.current.setQuery("dogs"));
    act(() => vi.advanceTimersByTime(300));

    expect(result.current.selectedIndex).toBe(-1);
  });
});
