import { nextTick } from "vue";
import { flushPromises, mount } from "@vue/test-utils";
import { describe, it, expect, vi } from "vitest";

import UListbox from "../UListbox.vue";
import UIcon from "../../ui.image-icon/UIcon.vue";
import UInputSearch from "../../ui.form-input-search/UInputSearch.vue";

import type { Props } from "../types";

describe("UListbox.vue", () => {
  const defaultOptions = [
    { label: "Option 1", value: "option1" },
    { label: "Option 2", value: "option2" },
    { label: "Option 3", value: "option3" },
  ];

  const highlightedClass = "bg-primary/5";

  describe("Props", () => {
    it("Model Value – sets initial value correctly for single selection", async () => {
      const initialValue = "option1";

      const component = mount(UListbox, {
        props: {
          modelValue: initialValue,
          options: defaultOptions,
        },
      });

      await flushPromises();
      const selectedIcon = component.findComponent(UIcon);

      expect(selectedIcon.exists()).toBe(true);
    });

    it("Model Value – sets initial value correctly for multiple selection", () => {
      const initialValue = ["option1", "option2"];

      const component = mount(UListbox, {
        props: {
          modelValue: initialValue,
          options: defaultOptions,
          multiple: true,
        },
      });

      const selectedIcons = component.findAllComponents(UIcon);

      expect(selectedIcons.length).toBe(2);
    });

    it("Model Value – updates value on option selection", async () => {
      const updatedValue = "option1";

      const component = mount(UListbox, {
        props: {
          modelValue: "",
          options: defaultOptions,
        },
      });

      const firstOption = component.find('[vl-key="option"]');

      await firstOption.trigger("click");

      expect(component.emitted("update:modelValue")![0][0]).toBe(updatedValue);
    });

    it("Options – renders all options correctly", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      const options = component.findAll('[vl-key="option"]');

      expect(options).toHaveLength(defaultOptions.length);

      options.forEach((option, index) => {
        expect(option.text()).toBe(defaultOptions[index].label);
      });
    });

    it("Options – handles empty options array", () => {
      const emptyOptionLabel = "No data to show";
      const expectedOptionsAmount = 1;

      const component = mount(UListbox, {
        props: {
          options: [],
        },
      });

      const options = component.findAll('[vl-key="option"]');

      expect(options).toHaveLength(expectedOptionsAmount);
      expect(options[0].text()).toContain(emptyOptionLabel);
    });

    it("Searchable – renders search input when searchable is true", () => {
      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
        },
      });

      const searchInput = component.findComponent(UInputSearch);

      expect(searchInput.exists()).toBe(true);
    });

    it("Searchable – does not render search input when searchable is false", () => {
      const component = mount(UListbox, {
        props: {
          searchable: false,
          options: defaultOptions,
        },
      });

      const searchInput = component.findComponent(UInputSearch);

      expect(searchInput.exists()).toBe(false);
    });

    it("Multiple – allows multiple selection when multiple is true", async () => {
      const component = mount(UListbox, {
        props: {
          multiple: true,
          modelValue: [],
          "onUpdate:modelValue": (value: string[]) => component.setProps({ modelValue: value }),
          options: defaultOptions,
        },
      });

      const options = component.findAll('[vl-key="option"]');

      await options[0].trigger("click");

      await options[1].trigger("click");

      expect(component.emitted("update:modelValue")![0][0]).toEqual([defaultOptions[0].value]);
      expect(component.emitted("update:modelValue")![1][0]).toEqual([
        defaultOptions[0].value,
        defaultOptions[1].value,
      ]);
    });

    it("Multiple – toggles selection when clicking selected option", async () => {
      const component = mount(UListbox, {
        props: {
          multiple: true,
          modelValue: ["option1"],
          "onUpdate:modelValue": (value: string[]) => component.setProps({ modelValue: value }),
          options: defaultOptions,
        },
      });

      const selectedOption = component.find('[vl-key="optionActive"]');

      await selectedOption.trigger("click");

      expect(component.emitted("update:modelValue")![0][0]).toEqual([]);
    });

    it("Label Key – uses correct label key for display", () => {
      const customOptions = [
        { title: "First Option", id: 1 },
        { title: "Second Option", id: 2 },
      ];

      const component = mount(UListbox, {
        props: {
          options: customOptions,
          labelKey: "title",
          valueKey: "id",
        },
      });

      const options = component.findAll('[vl-key="option"]');

      expect(options[0].text()).toBe(customOptions[0].title);
      expect(options[1].text()).toBe(customOptions[1].title);
    });

    it("Value Key – uses correct value key for selection", async () => {
      const customOptions = [
        { title: "First Option", id: 1 },
        { title: "Second Option", id: 2 },
      ];

      const updatedValue = 1;

      const component = mount(UListbox, {
        props: {
          options: customOptions,
          labelKey: "title",
          valueKey: "id",
        },
      });

      const firstOption = component.find('[vl-key="option"]');

      await firstOption.trigger("click");

      expect(component.emitted("update:modelValue")![0][0]).toBe(updatedValue);
    });

    it("Size – passes size prop to search input", () => {
      const size = "lg";

      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
          size: size as Props["size"],
        },
      });

      const searchInput = component.getComponent(UInputSearch);

      expect(searchInput.props("size")).toBe(size);
    });

    it("Color – applies correct color class to selected options", async () => {
      const color = "error";

      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
          modelValue: "option1",
          color: color as Props["color"],
        },
      });

      const firstOption = component.find('[vl-key="option"]');

      expect(firstOption.attributes("class")).toContain(color);
    });

    it("Disabled – disables all options when disabled is true", () => {
      const disabledOpacity = "--vl-disabled-opacity";

      const component = mount(UListbox, {
        props: {
          disabled: true,
          options: defaultOptions,
        },
      });

      const options = component.findAll('[vl-key="option"]');

      options.forEach((option) => {
        expect(option.attributes("class")).toContain(disabledOpacity);
      });
    });

    it("Disabled – does not emit update when disabled", async () => {
      const component = mount(UListbox, {
        props: {
          disabled: true,
          options: defaultOptions,
        },
      });

      const firstOption = component.find('[vl-key="option"]');

      await firstOption.trigger("click");

      expect(component.emitted("update:modelValue")).toBeFalsy();
    });

    it("Options Limit – limits number of displayed options", () => {
      const manyOptions = Array.from({ length: 10 }, (_, i) => ({
        label: `Option ${i}`,
        id: `option-${i}`,
      }));

      const optionsAmount = 5;

      const component = mount(UListbox, {
        props: {
          options: manyOptions,
          optionsLimit: optionsAmount,
        },
      });

      const options = component.findAll('[vl-key="option"]');

      expect(options).toHaveLength(optionsAmount);
    });

    it("Add Option – renders add button when addOption is true", () => {
      const component = mount(UListbox, {
        props: {
          addOption: true,
          options: defaultOptions,
        },
      });

      const addButton = component.find('[vl-key="addOptionButton"]');

      expect(addButton.exists()).toBe(true);
    });

    it("Add Option – emits add event when add button is clicked", async () => {
      const component = mount(UListbox, {
        props: {
          addOption: true,
          options: defaultOptions,
        },
      });

      const addButton = component.find('[vl-key="addOptionButton"]');

      await addButton.trigger("click");

      expect(component.emitted("add")).toBeTruthy();
    });

    it("Debounce – passes debounce prop to search input", () => {
      const debounce = 500;

      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
          debounce,
        },
      });

      const searchInput = component.getComponent(UInputSearch);

      expect(searchInput.props("debounce")).toBe(debounce);
    });

    it("Id – sets correct id attribute", () => {
      const id = "test-listbox";

      const component = mount(UListbox, {
        props: {
          id,
          searchable: true,
          options: defaultOptions,
        },
      });

      const searchInput = component.getComponent(UInputSearch);

      expect(searchInput.props("id")).toBe(id);
    });

    it("Data Test – applies the correct data-test attribute", () => {
      const dataTest = "test-listbox";

      const component = mount(UListbox, {
        props: {
          dataTest,
          options: defaultOptions,
          searchable: true,
          addOption: true,
        },
      });

      expect(component.find('[data-test="test-listbox-add"]').exists()).toBe(true);
      expect(component.find('[data-test="test-listbox-search"]').exists()).toBe(true);
      expect(component.find('[data-test="test-listbox-list"]').exists()).toBe(true);
      expect(component.find('[data-test="test-listbox-option"]').exists()).toBe(true);
    });

    it("Group Label Key – renders group labels correctly", () => {
      const groupedOptions = [
        { groupLabel: "Group 1", group: "group1" },
        { label: "Option 1", value: "option1", group: "group1" },
        { groupLabel: "Group 2", group: "group2" },
        { label: "Option 2", value: "option2", group: "group2" },
      ];

      const groupsAmount = 2;

      const component = mount(UListbox, {
        props: {
          options: groupedOptions,
          groupLabelKey: "group",
        },
      });

      const groupLabels = component.findAll('[data-group-label="true"]');

      expect(groupLabels).toHaveLength(groupsAmount);
      expect(groupLabels[0].text()).toBe(groupedOptions[0].groupLabel);
      expect(groupLabels[1].text()).toBe(groupedOptions[2].groupLabel);
    });

    it("Visible Options – limits visible options without scroll", async () => {
      const manyOptions = Array.from({ length: 10 }, (_, i) => ({
        label: `Option ${i}`,
        id: `option-${i}`,
      }));

      // Mock getComputedStyle to return non-zero heights for option elements
      const originalGetComputedStyle = window.getComputedStyle;

      vi.spyOn(window, "getComputedStyle").mockImplementation((el) => {
        const real = originalGetComputedStyle(el);

        return {
          ...real,
          height: "40px",
          marginTop: "0px",
          marginBottom: "4px",
          paddingTop: "4px",
          paddingBottom: "4px",
          borderTopWidth: "1px",
          borderBottomWidth: "1px",
          gap: "4px",
        } as CSSStyleDeclaration;
      });

      // Mock getBoundingClientRect for option elements
      vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({
        height: 40,
        width: 100,
        top: 0,
        left: 0,
        bottom: 40,
        right: 100,
        x: 0,
        y: 0,
        toJSON: () => {},
      });

      const component = mount(UListbox, {
        props: {
          options: manyOptions,
          visibleOptions: 3,
        },
      });

      await flushPromises();
      await nextTick();

      const wrapper = component.find('[vl-key="wrapper"]');
      const styleAttr = wrapper.attributes("style");
      const inlineMaxHeight = (wrapper.element as HTMLElement).style.maxHeight;

      expect(styleAttr ?? inlineMaxHeight).toContain("max-height:");

      vi.restoreAllMocks();
    });
  });

  describe("Functionality", () => {
    it("Search v-model – filters options using external search prop", async () => {
      const targetValue = "Option 2";

      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
          search: targetValue,
        },
      });

      await flushPromises();

      const options = component.findAll('[vl-key="option"]');

      expect(options).toHaveLength(1);
      expect(options[0].text()).toBe(targetValue);
    });

    it("Search v-model – emits update:search on input change", async () => {
      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
        },
      });

      const searchInput = component.getComponent(UInputSearch);

      await searchInput.setValue("Option 3");

      expect(component.emitted("update:search")).toBeTruthy();
      expect(component.emitted("update:search")![0][0]).toBe("Option 3");
    });

    it("Search – filters options based on search input", async () => {
      const targetValue = "Option 1";
      const filteredOptionsAmount = 1;

      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
        },
      });

      const searchInput = component.getComponent(UInputSearch);

      await searchInput.setValue(targetValue);
      await flushPromises();

      const options = component.findAll('[vl-key="option"]');

      expect(options).toHaveLength(filteredOptionsAmount);
      expect(options[0].text()).toBe(targetValue);
    });

    it("Search – emits search-change event on input", async () => {
      const targetValue = "test";

      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
        },
      });

      const searchInput = component.getComponent(UInputSearch);

      await searchInput.setValue(targetValue);

      expect(component.emitted("search-change")).toBeTruthy();
      expect(component.emitted("search-change")![0][0]).toBe(targetValue);
    });

    it("Search – emits search-blur event on blur", async () => {
      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
        },
      });

      const searchInput = component.getComponent(UInputSearch).get("input");

      await searchInput.trigger("blur");

      expect(component.emitted("search-blur")).toBeTruthy();
    });

    it("Keyboard Navigation – moves pointer down with arrow down", async () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      const wrapper = component.find('[vl-key="wrapper"]');

      await wrapper.trigger("focus");
      await wrapper.trigger("keydown", { key: "ArrowDown" });

      const options = component.findAll('[vl-key="option"]');

      expect(options[1].attributes("class")).toContain(highlightedClass);
    });

    it("Keyboard Navigation – moves pointer up with arrow up", async () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      const wrapper = component.find('[vl-key="wrapper"]');

      await wrapper.trigger("focus");
      await wrapper.trigger("keydown", { key: "ArrowDown" });
      await wrapper.trigger("keydown", { key: "ArrowUp" });

      const options = component.findAll('[vl-key="option"]');

      expect(options[0].attributes("class")).toContain(highlightedClass);
    });

    it("Keyboard Navigation – selects option with Enter key", async () => {
      const expectedValue = "option1";

      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      const wrapper = component.find('[vl-key="wrapper"]');

      await wrapper.trigger("keydown", { key: "Enter" });

      expect(component.emitted("update:modelValue")![0][0]).toBe(expectedValue);
    });

    it("Disabled Option – does not select disabled option", async () => {
      const optionsWithDisabled = [
        { label: "Option 1", id: "option1", disabled: true },
        { label: "Option 2", id: "option2" },
      ];

      const component = mount(UListbox, {
        props: {
          options: optionsWithDisabled,
        },
      });

      const firstOption = component.find('[vl-key="optionDisabled"]');

      await firstOption.trigger("click");

      expect(component.emitted("update:modelValue")).toBeFalsy();
    });

    it("Mouse Enter – sets pointer on mouseenter", async () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      const secondOption = component.findAll('[vl-key="option"]')[1];

      await secondOption.trigger("mouseenter");

      expect(secondOption.attributes("class")).toContain(highlightedClass);
    });
  });

  describe("Slots", () => {
    it("Option – renders custom option content", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
        slots: {
          option: '<span class="custom-option">Custom {{ params.option.label }}</span>',
        },
      });

      const customOption = component.find(".custom-option");

      expect(customOption.exists()).toBe(true);
      expect(customOption.text()).toBe("Custom Option 1");
    });

    it("Option – exposes option and index to slot", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
        slots: {
          option: "Option: {{ params.option.label }}, Index: {{ params.index }}",
        },
      });

      const firstOption = component.find('[vl-key="option"]');

      expect(firstOption.text()).toContain("Option: Option 1, Index: 0");
    });

    it("Before Option – renders content before option", () => {
      const slotContent = "Before";

      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
        slots: {
          "before-option": `<span class="before-option">${slotContent}</span>`,
        },
      });

      const beforeOption = component.find(".before-option");

      expect(beforeOption.exists()).toBe(true);
      expect(beforeOption.text()).toBe(slotContent);
    });

    it("After Option – renders content after option", () => {
      const slotContent = "After";

      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
        slots: {
          "after-option": `<span class="after-option">${slotContent}</span>`,
        },
      });

      const afterOption = component.find(".after-option");

      expect(afterOption.exists()).toBe(true);
      expect(afterOption.text()).toBe(slotContent);
    });

    it("After Option – shows selected icon for selected option by default", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
          modelValue: "option1",
        },
      });

      const selectedIcon = component.findComponent(UIcon);

      expect(selectedIcon.exists()).toBe(true);
    });

    it("Empty – renders custom empty state content", () => {
      const slotContent = "No options available";

      const component = mount(UListbox, {
        props: {
          options: [],
        },
        slots: {
          empty: `<span class="custom-empty">${slotContent}</span>`,
        },
      });

      const emptySlot = component.find(".custom-empty");

      expect(emptySlot.exists()).toBe(true);
      expect(emptySlot.text()).toBe(slotContent);
    });
  });

  describe("Events", () => {
    it("Add – emits when add button is clicked", async () => {
      const component = mount(UListbox, {
        props: {
          addOption: true,
          options: defaultOptions,
        },
      });

      const addButton = component.find('[vl-key="addOptionButton"]');

      await addButton.trigger("click");

      expect(component.emitted("add")).toBeTruthy();
    });

    it("Click Option – emits when option is clicked", async () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      const firstOption = component.find('[vl-key="option"]');

      await firstOption.trigger("click");

      expect(component.emitted("click-option")).toBeTruthy();
      expect(component.emitted("click-option")![0][0]).toEqual(defaultOptions[0]);
    });

    it("Search Change – emits when search input changes", async () => {
      const expectedValue = "test";

      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
        },
      });

      const searchInput = component.getComponent(UInputSearch);

      await searchInput.setValue("test");

      expect(component.emitted("search-change")).toBeTruthy();
      expect(component.emitted("search-change")![0][0]).toBe(expectedValue);
    });

    it("Search Blur – emits when search input loses focus", async () => {
      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
        },
      });

      const searchInput = component.getComponent(UInputSearch);

      await searchInput.get("input").trigger("blur");

      expect(component.emitted("search-blur")).toBeTruthy();
    });
  });

  describe("Exposed Properties", () => {
    it("Pointer Set – exposes pointerSet method", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      expect(component.vm.pointerSet).toBeDefined();
      expect(typeof component.vm.pointerSet).toBe("function");
    });

    it("Pointer Backward – exposes pointerBackward method", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      expect(component.vm.pointerBackward).toBeDefined();
      expect(typeof component.vm.pointerBackward).toBe("function");
    });

    it("Pointer Forward – exposes pointerForward method", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      expect(component.vm.pointerForward).toBeDefined();
      expect(typeof component.vm.pointerForward).toBe("function");
    });

    it("Pointer Reset – exposes pointerReset method", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      expect(component.vm.pointerReset).toBeDefined();
      expect(typeof component.vm.pointerReset).toBe("function");
    });

    it("Add Pointer Element – exposes addPointerElement method", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      expect(component.vm.addPointerElement).toBeDefined();
      expect(typeof component.vm.addPointerElement).toBe("function");
    });

    it("Options Ref – exposes optionsRef", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      expect(component.vm.optionsRef).toBeDefined();
    });

    it("Listbox Input Ref – exposes listboxInputRef", () => {
      const component = mount(UListbox, {
        props: {
          searchable: true,
          options: defaultOptions,
        },
      });

      expect(component.vm.listboxInputRef).toBeDefined();
    });

    it("Wrapper Ref – exposes wrapperRef", () => {
      const component = mount(UListbox, {
        props: {
          options: defaultOptions,
        },
      });

      expect(component.vm.wrapperRef).toBeDefined();
    });
  });
});
