// @vitest-environment happy-dom
// SearchResults is pure presentation — no React Query, no router.
// Mirror the IS_REACT_ACT_ENVIRONMENT flag from the other component
// tests so React doesn't log act() warnings during render/unmount.
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { describe, expect, it, vi } from "vitest";

import { SearchResults } from "./SearchResults";
import type { SearchResult, SearchResultsGrouped } from "@/hooks/useSearch";

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

function makeGrouped(
  claims: SearchResult[],
  remits: SearchResult[],
  activity: SearchResult[] = [],
  query = "ngu"
): SearchResultsGrouped {
  return {
    claims,
    remittances: remits,
    activity,
    total: claims.length + remits.length + activity.length,
    query,
  };
}

function claim(id: string, title = `Claim ${id}`): SearchResult {
  return {
    kind: "claim",
    id,
    title,
    subtitle: "subtitle",
    badge: "paid",
    score: 100,
  };
}
function remit(id: string): SearchResult {
  return {
    kind: "remittance",
    id,
    title: `Remit ${id}`,
    subtitle: "subtitle",
    badge: "received",
    score: 50,
  };
}
function activity(id: string, kind = "claim_paid"): SearchResult {
  return {
    kind: "activity",
    id,
    title: `Activity ${id}`,
    subtitle: "subtitle",
    badge: kind,
    score: 25,
  };
}

// ---------------------------------------------------------------------------
// Render helpers
// ---------------------------------------------------------------------------

function renderIntoContainer(element: React.ReactElement): {
  container: HTMLDivElement;
  unmount: () => void;
} {
  const container = document.createElement("div");
  document.body.appendChild(container);
  const root: Root = createRoot(container);
  act(() => {
    root.render(element);
  });
  return {
    container,
    unmount: () => {
      act(() => root.unmount());
      container.remove();
    },
  };
}

/** Find a result row by its data-result-index attribute (0-based). */
function rowAt(parent: HTMLElement, index: number): HTMLElement | null {
  return parent.querySelector(`[data-result-index="${index}"]`);
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("SearchResults", () => {
  it("renders the empty state when there are no results", () => {
    const { container, unmount } = renderIntoContainer(
      <SearchResults
        grouped={makeGrouped([], [], [], "")}
        selectedIndex={0}
        onSelect={vi.fn()}
        onHoverIndex={vi.fn()}
      />
    );
    // The listbox role is always present so screen readers can find
    // it; only the empty state's text differentiates "no results".
    expect(container.querySelector('[role="listbox"]')).not.toBeNull();
    expect(container.textContent).toContain("Type to search");
    unmount();
  });

  it("renders the query in the empty state when results are empty but a query exists", () => {
    const { container, unmount } = renderIntoContainer(
      <SearchResults
        grouped={makeGrouped([], [], [], "nope")}
        selectedIndex={0}
        onSelect={vi.fn()}
        onHoverIndex={vi.fn()}
      />
    );
    expect(container.textContent).toContain("nope");
    unmount();
  });

  it("renders group headers with counts", () => {
    const { container, unmount } = renderIntoContainer(
      <SearchResults
        grouped={makeGrouped(
          [claim("CLM-1"), claim("CLM-2")],
          [remit("REM-1")]
        )}
        selectedIndex={0}
        onSelect={vi.fn()}
        onHoverIndex={vi.fn()}
      />
    );
    expect(container.textContent).toContain("Claims · 2");
    expect(container.textContent).toContain("Remittances · 1");
    unmount();
  });

  it("renders one row per result, in flat display order", () => {
    const { container, unmount } = renderIntoContainer(
      <SearchResults
        grouped={makeGrouped(
          [claim("CLM-1"), claim("CLM-2")],
          [remit("REM-1"), remit("REM-2")],
          [activity("ACT-1")]
        )}
        selectedIndex={0}
        onSelect={vi.fn()}
        onHoverIndex={vi.fn()}
      />
    );
    // 5 rows total, indexed 0..4, with kinds in order: claim, claim,
    // remit, remit, activity.
    expect(rowAt(container, 0)?.getAttribute("data-result-kind")).toBe("claim");
    expect(rowAt(container, 1)?.getAttribute("data-result-kind")).toBe("claim");
    expect(rowAt(container, 2)?.getAttribute("data-result-kind")).toBe("remittance");
    expect(rowAt(container, 3)?.getAttribute("data-result-kind")).toBe("remittance");
    expect(rowAt(container, 4)?.getAttribute("data-result-kind")).toBe("activity");
    // The data-result-id should round-trip.
    expect(rowAt(container, 0)?.getAttribute("data-result-id")).toBe("CLM-1");
    expect(rowAt(container, 4)?.getAttribute("data-result-id")).toBe("ACT-1");
    unmount();
  });

  it("marks the selected row with data-selected", () => {
    const { container, unmount } = renderIntoContainer(
      <SearchResults
        grouped={makeGrouped([claim("CLM-1"), claim("CLM-2")], [])}
        selectedIndex={1}
        onSelect={vi.fn()}
        onHoverIndex={vi.fn()}
      />
    );
    expect(rowAt(container, 0)?.hasAttribute("data-selected")).toBe(false);
    expect(rowAt(container, 1)?.hasAttribute("data-selected")).toBe(true);
    unmount();
  });

  it("calls onSelect with the clicked result on mousedown", () => {
    const onSelect = vi.fn();
    const c = claim("CLM-99");
    const { container, unmount } = renderIntoContainer(
      <SearchResults
        grouped={makeGrouped([c], [])}
        selectedIndex={0}
        onSelect={onSelect}
        onHoverIndex={vi.fn()}
      />
    );
    const row = rowAt(container, 0)!;
    expect(row).not.toBeNull();
    act(() => {
      row.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
    });
    expect(onSelect).toHaveBeenCalledWith(c);
    unmount();
  });

  it("calls onHoverIndex with the row index on mouseenter", () => {
    const onHover = vi.fn();
    const { container, unmount } = renderIntoContainer(
      <SearchResults
        grouped={makeGrouped(
          [claim("CLM-1"), claim("CLM-2"), claim("CLM-3")],
          []
        )}
        selectedIndex={0}
        onSelect={vi.fn()}
        onHoverIndex={onHover}
      />
    );
    act(() => {
      // React's synthetic `onMouseEnter` is implemented on top of the
      // native `mouseover` event (which DOES bubble) — the native
      // `mouseenter` event does NOT bubble, so dispatching it here
      // would never reach React's listener. `mouseover` with
      // bubbles:true is the canonical way to trigger React's
      // `onMouseEnter` in a unit test.
      rowAt(container, 2)!.dispatchEvent(
        new MouseEvent("mouseover", { bubbles: true })
      );
    });
    expect(onHover).toHaveBeenCalledWith(2);
    unmount();
  });

  it("exposes a focusIndex imperative handle that scrolls the matching row into view", () => {
    // Replace scrollIntoView on the prototype so we can spy on it
    // without a real scrollable container. happy-dom doesn't actually
    // lay out the page so the call would be a no-op otherwise.
    const scrollSpy = vi.fn();
    const proto = HTMLElement.prototype as unknown as {
      scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
    };
    const original = proto.scrollIntoView;
    proto.scrollIntoView = scrollSpy;

    try {
      const ref = React.createRef<{ focusIndex: (i: number) => void }>();
      const { unmount } = renderIntoContainer(
        <SearchResults
          ref={ref}
          grouped={makeGrouped(
            [claim("CLM-1"), claim("CLM-2"), claim("CLM-3")],
            []
          )}
          selectedIndex={0}
          onSelect={vi.fn()}
          onHoverIndex={vi.fn()}
        />
      );
      act(() => {
        ref.current?.focusIndex(2);
      });
      expect(scrollSpy).toHaveBeenCalled();
      unmount();
    } finally {
      proto.scrollIntoView = original;
    }
  });

  it("only renders groups that have at least one result", () => {
    const { container, unmount } = renderIntoContainer(
      <SearchResults
        grouped={makeGrouped(
          [claim("CLM-1")],
          [],
          []
        )}
        selectedIndex={0}
        onSelect={vi.fn()}
        onHoverIndex={vi.fn()}
      />
    );
    expect(container.textContent).toContain("Claims · 1");
    expect(container.textContent).not.toContain("Remittances");
    expect(container.textContent).not.toContain("Activity ·");
    unmount();
  });

  it("renders rows with role=option and aria-selected", () => {
    const { container, unmount } = renderIntoContainer(
      <SearchResults
        grouped={makeGrouped([claim("CLM-1"), claim("CLM-2")], [])}
        selectedIndex={0}
        onSelect={vi.fn()}
        onHoverIndex={vi.fn()}
      />
    );
    const rows = container.querySelectorAll('[role="option"]');
    expect(rows.length).toBe(2);
    expect(rows[0]!.getAttribute("aria-selected")).toBe("true");
    expect(rows[1]!.getAttribute("aria-selected")).toBe("false");
    unmount();
  });
});