import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { Lookup } from "@/components/ui/Lookup";

type Person = { id: string; name: string };

const PEOPLE: Person[] = [
  { id: "1", name: "Katharina Aigner" },
  { id: "2", name: "Kurt Aigner" },
  { id: "3", name: "Simon Aigner" },
];

function renderLookup(onSelect = vi.fn()) {
  const onSearch = vi.fn(async (q: string) => PEOPLE.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())));
  render(<Lookup<Person> onSearch={onSearch} onSelect={onSelect} renderResult={(p) => <span>{p.name}</span>} placeholder="Suchen…" />);
  return { onSelect, input: screen.getByRole("combobox") };
}

// The component was a text input with a div of clickable buttons under it:
// typeable, but no keyboard path to a result and nothing announcing that a
// list had appeared.
describe("Lookup keyboard operation", () => {
  it("exposes combobox semantics", async () => {
    const { input } = renderLookup();
    expect(input).toHaveAttribute("aria-expanded", "false");
    await userEvent.type(input, "Aigner");
    expect(await screen.findByRole("listbox")).toBeInTheDocument();
    expect(input).toHaveAttribute("aria-expanded", "true");
    expect(await screen.findAllByRole("option")).toHaveLength(3);
  });

  it("moves the selection with the arrow keys", async () => {
    const { input } = renderLookup();
    await userEvent.type(input, "Aigner");
    await screen.findByRole("listbox");

    const options = screen.getAllByRole("option");
    expect(options[0]).toHaveAttribute("aria-selected", "true");

    await userEvent.keyboard("{ArrowDown}");
    expect(screen.getAllByRole("option")[1]).toHaveAttribute("aria-selected", "true");
    // aria-activedescendant is what tells a screen reader which row is
    // current while focus stays in the input.
    expect(input).toHaveAttribute("aria-activedescendant", screen.getAllByRole("option")[1].id);
  });

  it("wraps around at the ends", async () => {
    const { input } = renderLookup();
    await userEvent.type(input, "Aigner");
    await screen.findByRole("listbox");

    await userEvent.keyboard("{ArrowUp}");
    expect(screen.getAllByRole("option")[2]).toHaveAttribute("aria-selected", "true");
    await userEvent.keyboard("{ArrowDown}");
    expect(screen.getAllByRole("option")[0]).toHaveAttribute("aria-selected", "true");
  });

  it("selects the highlighted result with Enter", async () => {
    const { input, onSelect } = renderLookup();
    await userEvent.type(input, "Aigner");
    await screen.findByRole("listbox");

    await userEvent.keyboard("{ArrowDown}{Enter}");
    expect(onSelect).toHaveBeenCalledWith(PEOPLE[1]);
    // Clears itself afterwards, ready for the next search.
    expect(input).toHaveValue("");
  });

  it("closes on Escape without selecting", async () => {
    const { input, onSelect } = renderLookup();
    await userEvent.type(input, "Aigner");
    await screen.findByRole("listbox");

    await userEvent.keyboard("{Escape}");
    expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
    expect(onSelect).not.toHaveBeenCalled();
  });

  it("does not open below the minimum query length", async () => {
    const { input } = renderLookup();
    await userEvent.type(input, "A");
    expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
  });

  it("still selects on click", async () => {
    const { input, onSelect } = renderLookup();
    await userEvent.type(input, "Kurt");
    await screen.findByRole("listbox");
    await userEvent.click(screen.getByRole("option", { name: "Kurt Aigner" }));
    expect(onSelect).toHaveBeenCalledWith(PEOPLE[1]);
  });
});
