// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck The types for this file don't work at all because of the
// test groupings.
import { act, fireEvent, render, screen, within } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { Combobox } from "./Combobox";
import {
  FRUITS,
  FRUITS_OBJECT_GROUPS,
  FRUITS_OBJECTS,
  FRUITS_STRING_GROUPS,
} from "./fixtures";
import type { ComboboxOption, ComboboxOptions } from "./types";
import { isOptionGroup } from "./utils";

function getFirstOption<T extends ComboboxOption>(options: ComboboxOptions<T>) {
  if (isOptionGroup(options)) {
    return options[0].options[0];
  }
  return options[0];
}

function getFirstOptionValue<T extends ComboboxOption>(
  options: ComboboxOptions<T>,
) {
  const firstOption = getFirstOption(options);
  if (typeof firstOption === "string") {
    return firstOption;
  }
  return firstOption.value;
}

function getFlatOptions(options: ComboboxOptions) {
  if (isOptionGroup(options)) {
    return options.flatMap(group => group.options);
  }
  return options;
}

let user;

describe("Combobox", () => {
  beforeEach(() => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    user = userEvent.setup({
      advanceTimers: vi.advanceTimersByTime.bind(vi),
    });
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe("common functionality", () => {
    describe.each([
      ["string", FRUITS],
      ["object", FRUITS_OBJECTS],
      ["string groups", FRUITS_STRING_GROUPS],
      ["object groups", FRUITS_OBJECT_GROUPS],
    ])("Combobox with %s options", (_, options) => {
      it("should render without crashing", () => {
        render(<Combobox label="Fruit" options={options} />);
      });

      it("should render a combobox", () => {
        render(<Combobox label="Fruit" options={options} />);
        const input = screen.getByRole("combobox");
        expect(input).toBeInTheDocument();
      });

      it("should display a label if set", () => {
        render(<Combobox label="Fruit" options={options} />);
        const label = screen.getByLabelText("Fruit");
        expect(label).toBeInTheDocument();
      });

      it("should pass id to wrapper div if set", () => {
        render(<Combobox label="Fruit" options={options} id="custom" />);

        const component = screen.getByTestId("mobius-combobox__wrapper");
        expect(component).toHaveAttribute("id", "custom");
      });

      it("should use the default value provided", () => {
        render(
          <Combobox label="Fruit" options={options} defaultValue="Apple" />,
        );
        const input = screen.getByRole("combobox");
        expect(input.value).toBe("Apple");
      });

      it("should accept a ref and forward it to the input", () => {
        const ref = { current: null };
        render(<Combobox label="Fruit" options={options} ref={ref} />);
        expect(ref.current).not.toBeNull();
      });

      it("should not render a listbox by default", () => {
        render(<Combobox label="Fruit" options={options} />);
        const listbox = screen.queryByRole("listbox");
        expect(listbox).toBeInTheDocument();
        expect(listbox).toHaveClass("mobius-combobox__list--hidden");
      });

      it("should set placeholder in input if provided", () => {
        render(
          <Combobox
            label="Fruit"
            options={options}
            placeholder="Please pick a fruit"
          />,
        );
        const input = screen.getByRole("combobox");
        expect(input).toHaveAttribute("placeholder", "Please pick a fruit");
      });

      it("should give each option a unique id", () => {
        render(<Combobox label="Fruit" options={options} />);
        const input = screen.getByRole("combobox");
        fireEvent.mouseDown(input);
        fireEvent.focus(input);
        const visibleOptions = screen.getAllByRole("option");
        const ids = new Set(
          visibleOptions.map(option => option.getAttribute("id")),
        );

        expect(ids.size).toBe(getFlatOptions(options).length);
      });

      it("should give each option a data-testid based on its value", () => {
        render(<Combobox label="Fruit" options={options} />);
        const input = screen.getByRole("combobox");
        fireEvent.mouseDown(input);
        fireEvent.focus(input);
        const firstOption = screen.getAllByRole("option")[0];
        const testId = firstOption.getAttribute("data-testid");
        expect(testId).toMatch(/^combobox-option-.+$/);
      });

      it("should use a custom optionTestIdPrefix when provided", () => {
        render(
          <Combobox
            label="Fruit"
            options={options}
            optionTestIdPrefix="trade-option"
          />,
        );
        const input = screen.getByRole("combobox");
        fireEvent.mouseDown(input);
        fireEvent.focus(input);
        const firstOption = screen.getAllByRole("option")[0];
        const testId = firstOption.getAttribute("data-testid");
        expect(testId).toMatch(/^trade-option-.+$/);
      });

      it("should render custom option component when optionComponent is provided", async () => {
        const CustomOption = ({
          option,
          isHighlighted,
        }: {
          option: string;
          isHighlighted: boolean;
        }) => (
          <div data-testid="custom-option">
            {typeof option === "string" ? option : option.label}
            {isHighlighted ? " (highlighted)" : ""}
          </div>
        );

        render(
          <Combobox
            label="Fruit"
            options={options}
            optionComponent={CustomOption}
          />,
        );

        const input = screen.getByRole("combobox");
        await user.click(input);

        const customOptions = screen.getAllByTestId("custom-option");

        expect(customOptions).toHaveLength(getFlatOptions(options).length);

        await user.keyboard("{arrowdown}");
        expect(screen.getByText(/\(highlighted\)/)).toBeInTheDocument();
      });

      describe("mouse interactions", () => {
        it("should render a listbox when the input is focused", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          const listbox = screen.getByRole("listbox");
          expect(listbox).toBeInTheDocument();
        });

        it("should not render a listbox if there are no options", () => {
          render(<Combobox label="Fruit" options={[]} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          const listbox = screen.queryByRole("listbox");
          expect(listbox).toBeInTheDocument();
          expect(listbox).toHaveClass("mobius-combobox__list--hidden");
        });

        it("should call the onSelected callback when an option is selected", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
            />,
          );
          const input = screen.getByRole("combobox");
          await user.click(input);
          const visibleOptions = screen.getAllByRole("option");
          await user.click(visibleOptions[0]);
          expect(onSelected).toHaveBeenCalledWith(getFirstOption(options));
        });

        it("should call the onSelected callback when a user blurs and the typed text matches an option label", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
            />,
          );
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.type(input, "apple");
          await user.tab();
          expect(onSelected).toHaveBeenCalledWith(getFirstOption(options));
        });

        it("should show validation error and clear input when a user blurs with invalid text", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
            />,
          );
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.type(input, "app");
          await act(async () => {
            await user.tab();
            vi.advanceTimersByTime(200);
          });
          await vi.waitFor(() => {
            expect(onSelected).not.toHaveBeenCalled();
            expect(input).toHaveValue("");
            expect(
              screen.getByText("Please select an option from the list"),
            ).toBeInTheDocument();
          });
        });

        it("should call onSelected with empty value when user clears input and blurs", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
              defaultValue="Apple"
            />,
          );
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.clear(input);
          await act(async () => {
            await user.tab();
            vi.advanceTimersByTime(200);
          });
          await vi.waitFor(() => {
            // String options emit "", object options emit { label: "", value: "" }
            const firstOption = getFirstOption(options);
            const expectedEmpty =
              typeof firstOption === "string" ? "" : { label: "", value: "" };
            expect(onSelected).toHaveBeenCalledWith(expectedEmpty);
            expect(input).toHaveValue("");
          });
        });

        it("should clear validation error when user starts typing again", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.type(input, "xyz");
          await act(async () => {
            await user.tab();
            vi.advanceTimersByTime(200);
          });
          await vi.waitFor(() => {
            expect(
              screen.getByText("Please select an option from the list"),
            ).toBeInTheDocument();
          });

          // Start typing again
          await user.click(input);
          await user.type(input, "a");
          await vi.waitFor(() => {
            expect(
              screen.queryByText("Please select an option from the list"),
            ).not.toBeInTheDocument();
          });
        });

        it("should update the input value when an option is selected", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          const visibleOptions = screen.getAllByRole("option");
          await user.click(visibleOptions[0]);

          expect(input).toHaveValue(getFirstOptionValue(options));
        });

        it("should close the listbox when the input is blurred", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          await act(async () => {
            await user.tab();
            vi.advanceTimersByTime(200);
          });
          const listbox = screen.queryByRole("listbox");
          expect(listbox).toBeInTheDocument();
          expect(listbox).toHaveClass("mobius-combobox__list--hidden");
        });

        it("should close the listbox when an option is selected", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          const visibleOptions = screen.getAllByRole("option");
          await user.click(visibleOptions[0]);
          const listbox = screen.queryByRole("listbox");
          expect(listbox).toBeInTheDocument();
          expect(listbox).toHaveClass("mobius-combobox__list--hidden");
        });

        it("should close the listbox when clicking outside", async () => {
          render(
            <div>
              <Combobox label="Fruit" options={options} />
              <button type="button">Outside</button>
            </div>,
          );
          const input = screen.getByRole("combobox");
          const outside = screen.getByRole("button");
          await user.click(input);
          await user.click(outside);
          await act(async () => {
            await user.tab();
            vi.advanceTimersByTime(200);
          });
          const listbox = screen.queryByRole("listbox");
          expect(listbox).toBeInTheDocument();
          expect(listbox).toHaveClass("mobius-combobox__list--hidden");
        });

        it("should not close the listbox on right click", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.pointer({ target: input, keys: "[MouseRight]" });
          const listbox = screen.getByRole("listbox");
          expect(listbox).toBeInTheDocument();
        });

        describe("custom events", () => {
          it("should call an onBlur event if passed", async () => {
            const onBlur = vi.fn();
            render(
              <Combobox label="Fruit" options={options} onBlur={onBlur} />,
            );
            const input = screen.getByRole("combobox");
            await user.click(input);
            await user.type(input, "app");

            await act(async () => {
              await user.tab();
              vi.advanceTimersByTime(200);
            });
            await Promise.resolve();
            expect(onBlur).toHaveBeenCalled();
          });

          it("should call an onFocus event if passed", async () => {
            const onFocus = vi.fn();
            render(
              <Combobox label="Fruit" options={options} onFocus={onFocus} />,
            );
            const input = screen.getByRole("combobox");
            await user.click(input);
            expect(onFocus).toHaveBeenCalled();
          });

          it("should call an onChange event if passed", async () => {
            const onChange = vi.fn();
            render(
              <Combobox label="Fruit" options={options} onChange={onChange} />,
            );
            const input = screen.getByRole("combobox");
            await user.click(input);
            await user.click(input);
            await user.type(input, "app");
            expect(onChange).toHaveBeenCalled();
          });
        });
      });

      describe("keyboard interactions", () => {
        it("should show the listbox when the input is focused", async () => {
          render(<Combobox label="Fruit" options={options} />);
          await user.tab();
          const listbox = screen.getByRole("listbox");
          expect(listbox).toBeInTheDocument();
        });

        it("should not show the listbox when the input is focused with no options", async () => {
          render(<Combobox label="Fruit" options={[]} />);
          await user.tab();
          const listbox = screen.queryByRole("listbox");
          expect(listbox).toBeInTheDocument();
          expect(listbox).toHaveClass("mobius-combobox__list--hidden");
        });

        it("should select the top option when pressing Enter", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
            />,
          );
          const input = screen.getByRole("combobox");
          await user.type(input, "Ap");
          await user.type(input, "{enter}");
          expect(onSelected).toHaveBeenCalledWith(getFirstOption(options));
        });

        it("should not select an option when pressing Enter with no options", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
            />,
          );
          const input = screen.getByRole("combobox");
          await user.type(input, "Zz");
          await user.type(input, "{enter}");
          expect(onSelected).not.toHaveBeenCalled();
        });

        it("should select the highlighted option when pressing Enter", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
            />,
          );
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.keyboard("{arrowdown}");
          await user.keyboard("{enter}");
          expect(onSelected).toHaveBeenCalledWith(getFirstOption(options));
        });

        it("should open the listbox when pressing ArrowDown", async () => {
          render(<Combobox label="Fruit" options={options} />);
          screen.getByRole("combobox");
          await user.tab();
          await user.keyboard("{arrowdown}");
          const listbox = screen.getByRole("listbox");
          expect(listbox).toBeInTheDocument();
        });

        it("should highlight the first option when pressing ArrowDown", async () => {
          render(<Combobox label="Fruit" options={options} />);
          screen.getByRole("combobox");
          await user.tab();
          await user.keyboard("{arrowdown}");
          const visibleOptions = screen.getAllByRole("option");
          expect(visibleOptions[0]).toHaveAttribute("aria-selected", "true");
        });

        it("should open the listbox when pressing ArrowUp", async () => {
          render(<Combobox label="Fruit" options={options} />);
          screen.getByRole("combobox");
          await user.tab();
          await user.keyboard("{arrowup}");
          const listbox = screen.getByRole("listbox");
          expect(listbox).toBeInTheDocument();
        });

        it("should close the listbox when pressing Escape", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.keyboard("{escape}");
          const listbox = screen.queryByRole("listbox");
          expect(listbox).toBeInTheDocument();
          expect(listbox).toHaveClass("mobius-combobox__list--hidden");
        });

        it("should clear the input when pressing Escape", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.type(input, "Ap");
          expect(input.value).toEqual("Ap");
          await user.keyboard("{escape}");
          expect(input.value).toEqual("");
        });

        it("should not call onSelected when pressing Escape", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
            />,
          );
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.keyboard("{escape}");
          expect(onSelected).not.toHaveBeenCalled();
        });

        it("should reset the highlighted index when the input value changes", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.keyboard("{arrowdown}");
          await user.type(input, "Ap");
          const visibleOptions = screen.getAllByRole("option");
          expect(visibleOptions[0]).toHaveAttribute("aria-selected", "true");
        });

        it("should select the first option when pressing Home", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
            />,
          );
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.keyboard("{arrowdown}{arrowdown}");
          await user.keyboard("{home}");
          const visibleOptions = screen.getAllByRole("option");
          expect(visibleOptions[0]).toHaveAttribute("aria-selected", "true");
        });

        it("should select the last option when pressing End", async () => {
          const onSelected = vi.fn();
          render(
            <Combobox
              label="Fruit"
              options={options}
              onSelected={onSelected}
            />,
          );
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.keyboard("{end}");
          const visibleOptions = screen.getAllByRole("option");
          expect(visibleOptions[visibleOptions.length - 1]).toHaveAttribute(
            "aria-selected",
            "true",
          );
        });
      });

      describe("filtering", () => {
        it("should render all of the options in the listbox", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          const visibleOptions = screen.getAllByRole("option");

          expect(visibleOptions).toHaveLength(getFlatOptions(options).length);
        });

        it("should filter the options based on the input value", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.type(input, "Ap");
          const visibleOptions = screen.getAllByRole("option");
          expect(visibleOptions).toHaveLength(6);
        });

        it("should ignore case when filtering options", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.type(input, "AP");
          const visibleOptions = screen.getAllByRole("option");
          expect(visibleOptions).toHaveLength(6);
        });
      });

      describe("classnames", () => {
        it("should have the correct classnames for the combobox", () => {
          const { container } = render(
            <Combobox label="Fruit" options={options} />,
          );
          expect(container.firstChild).toHaveClass("mobius mobius-combobox");
        });

        it("should have the correct classnames for the input", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          expect(input).toHaveClass("mobius-combobox__input");
        });

        it("should have the correct classnames for the listbox", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          const listbox = screen.getByRole("listbox");
          expect(listbox).toHaveClass("mobius-combobox__list");
        });

        it("should have the correct classnames for the options including personal data identifier classname (has-pii)", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          const visibleOptions = screen.getAllByRole("option");
          visibleOptions.forEach(option => {
            expect(option).toHaveClass("mobius-combobox__option has-pii");
          });
        });

        it("should apply the highlighted class to the highlighted option", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          await user.keyboard("{arrowdown}");
          const visibleOptions = screen.getAllByRole("option");
          expect(visibleOptions[0]).toHaveClass(
            "mobius-combobox__option--is-highlighted",
          );
        });

        it("should add the mobius-combobox--is-expanded class when the listbox is open", () => {
          render(<Combobox label="Fruit" options={options} />);
          const wrapper = screen.getByTestId("mobius-combobox__wrapper");
          const input = screen.getByRole("combobox");
          expect(wrapper).not.toHaveClass("mobius-combobox--is-expanded");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          expect(wrapper).toHaveClass("mobius-combobox--is-expanded");
        });

        it("should support custom class names", () => {
          const customClassName = "my-class";
          const { container } = render(
            <Combobox
              label="Fruit"
              options={options}
              className={customClassName}
            />,
          );

          expect(container.firstChild).toHaveClass(customClassName);
        });
      });

      describe("ARIA", () => {
        it("should set aria-autocomplete to list", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          expect(input).toHaveAttribute("aria-autocomplete", "list");
        });

        it("should set aria-haspopup to listbox", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          expect(input).toHaveAttribute("aria-haspopup", "listbox");
        });

        it("should set aria-controls to the listbox id", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          const listbox = screen.getByRole("listbox");
          expect(input).toHaveAttribute("aria-controls", listbox.id);
        });

        it("should set aria-expanded to false by default", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          expect(input).toHaveAttribute("aria-expanded", "false");
        });

        it("should set aria-expanded to true when the listbox is open", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          expect(input).toHaveAttribute("aria-expanded", "true");
        });

        it("should set aria-selected to false for all options except first option", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          const visibleOptions = screen.getAllByRole("option");
          visibleOptions.forEach(option => {
            expect(option).toHaveAttribute("aria-selected", "false");
          });
        });

        it("should set aria-selected to true for the highlighted option", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.click(input);
          const visibleOptions = screen.getAllByRole("option");
          await user.keyboard("{arrowdown}");
          expect(visibleOptions[0]).toHaveAttribute("aria-selected", "true");
        });

        it("should set aria-activedescendant based on the highlightedIndex and highlightedGroupIndex", async () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          await user.tab();
          await user.keyboard("{arrowdown}");
          const visibleOptions = screen.getAllByRole("option");
          expect(input).toHaveAttribute(
            "aria-activedescendant",
            `${visibleOptions[0].id}`,
          );
        });
      });
    });
  });

  describe("groups", () => {
    describe.each([
      ["string groups", FRUITS_STRING_GROUPS],
      ["object groups", FRUITS_OBJECT_GROUPS],
    ])("Combobox with %s options", (_, options) => {
      it("should render all of the groups in the listbox", () => {
        render(<Combobox label="Fruit" options={options} />);
        const input = screen.getByRole("combobox");
        fireEvent.mouseDown(input);
        fireEvent.focus(input);
        const groups = screen.getAllByRole("group");
        expect(groups).toHaveLength(options.length);
      });

      it("should render all of the options in each group", () => {
        render(<Combobox label="Fruit" options={options} />);
        const input = screen.getByRole("combobox");
        fireEvent.mouseDown(input);
        fireEvent.focus(input);
        options.forEach(group => {
          const groupElement = screen.getByRole("group", {
            name: group.heading,
          });
          const visibleOptions = within(groupElement).queryAllByRole("option");
          expect(visibleOptions).toHaveLength(group.options.length);
        });
      });

      describe("classnames", () => {
        it("should have the correct classnames for the groups", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          const groups = screen.getAllByRole("group");
          groups.forEach(group => {
            expect(group).toHaveClass("mobius-combobox__group");
          });
        });

        it("should have the correct classnames for the group labels", () => {
          render(<Combobox label="Fruit" options={options} />);
          const input = screen.getByRole("combobox");
          fireEvent.mouseDown(input);
          fireEvent.focus(input);
          const groupLabels = screen
            .getAllByRole("group")
            .map(group => group.firstChild);
          groupLabels.forEach(label => {
            expect(label).toHaveClass("mobius-combobox__group-label");
          });
        });
      });
    });
  });

  describe("async", () => {
    describe.each([
      ["string", FRUITS],
      ["object", FRUITS_OBJECTS],
      ["string groups", FRUITS_STRING_GROUPS],
      ["object groups", FRUITS_OBJECT_GROUPS],
    ])("Combobox with %s options", (_, options) => {
      const asyncOptions = async (input: string) => {
        await new Promise(resolve => {
          setTimeout(resolve, 1000);
        });
        if (isOptionGroup(options)) {
          return options.map(group => ({
            heading: group.heading,
            options: group.options.filter(option =>
              typeof option === "string"
                ? option.includes(input)
                : option.label.includes(input),
            ),
          }));
        }
        return options.filter(option =>
          typeof option === "string"
            ? option.includes(input)
            : option.label.includes(input),
        );
      };

      it("should render without crashing", () => {
        render(<Combobox label="Fruit" asyncOptions={asyncOptions} />);
      });

      it("should render the options after the async function resolves", async () => {
        render(<Combobox label="Fruit" asyncOptions={asyncOptions} />);
        const input = screen.getByRole("combobox");
        await act(async () => {
          await user.type(input, "berry");
          await vi.advanceTimersByTimeAsync(2000);
        });
        await act(async () => {
          await vi.advanceTimersByTimeAsync(2000);
        });
        const visibleOptions = screen.getAllByRole("option");
        expect(visibleOptions).toHaveLength(7);
      });

      it("should render the loading indicator when the async function is running", async () => {
        render(<Combobox label="Fruit" asyncOptions={asyncOptions} />);
        const input = screen.getByRole("combobox");
        await act(async () => {
          await user.type(input, "berry");
          vi.advanceTimersByTime(500);
        });
        const wrapper = screen.getByTestId("mobius-combobox__wrapper");
        expect(wrapper).toHaveClass("mobius-combobox--is-loading");
      });

      it("should not render the loading indicator when the async function is done", async () => {
        render(<Combobox label="Fruit" asyncOptions={asyncOptions} />);
        const input = screen.getByRole("combobox");
        await act(async () => {
          await user.type(input, "berry");
          await vi.advanceTimersByTimeAsync(2000);
        });
        await act(async () => {
          await vi.advanceTimersByTimeAsync(2000);
        });
        const wrapper = screen.getByTestId("mobius-combobox__wrapper");
        expect(wrapper).not.toHaveClass("mobius-combobox--is-loading");
      });

      it("should call onSelected with empty value when user clears async input and blurs", async () => {
        const onSelected = vi.fn();
        render(
          <Combobox
            label="Fruit"
            asyncOptions={asyncOptions}
            onSelected={onSelected}
            defaultValue="Apple"
          />,
        );
        const input = screen.getByRole("combobox");
        await user.click(input);
        await user.clear(input);
        await user.tab();
        act(() => {
          vi.advanceTimersByTime(200);
        });
        await vi.waitFor(() => {
          // Async combobox needs to infer type - check what was actually called
          // For empty values, it looks at the first option from the last loaded set
          // Since we're testing across different option types, just verify empty was called
          expect(onSelected).toHaveBeenCalled();
          const callArg = onSelected.mock.calls[0][0];
          // Verify it's an empty value (either "" or { label: "", value: "" })
          if (typeof callArg === "string") {
            expect(callArg).toBe("");
          } else {
            expect(callArg).toEqual({ label: "", value: "" });
          }
          expect(input).toHaveValue("");
        });
      });

      it("should show validation error when user enters invalid text in async combobox", async () => {
        const onSelected = vi.fn();
        render(
          <Combobox
            label="Fruit"
            asyncOptions={asyncOptions}
            onSelected={onSelected}
          />,
        );
        const input = screen.getByRole("combobox");
        await user.click(input);
        await user.type(input, "berry");

        // Wait for async options to load
        await act(async () => {
          await vi.advanceTimersByTimeAsync(1100);
        });

        // Clear and type invalid text
        await user.clear(input);
        await user.type(input, "xyz");

        // Wait for async to complete with no results
        await act(async () => {
          await vi.advanceTimersByTimeAsync(1100);
        });

        // Blur to trigger validation
        await user.tab();
        act(() => {
          vi.advanceTimersByTime(200);
        });

        await vi.waitFor(() => {
          expect(
            screen.getByText("Please select an option from the list"),
          ).toBeInTheDocument();
          expect(input).toHaveValue("");
        });
      });
    });
  });
});
