import { describe, test, expect, afterEach, vi } from "vitest";
import { enableAutoUnmount, mount } from "@vue/test-utils";
import { nextTick } from "vue";
import { setTimeout } from "timers/promises";

import type { OptionsProp } from "@/composables";

import OAutocomplete from "@/components/autocomplete/Autocomplete.vue";
import type { AutocompleteOptions } from "../props";
import type { DropdownItemProps } from "@/components/dropdown/props";

describe("OAutocomplete tests", () => {
    enableAutoUnmount(afterEach);

    const OPTIONS = [
        "Angular",
        "Angular 2",
        "Aurelia",
        "Backbone",
        "Ember",
        "jQuery",
        "Meteor",
        "Node.js",
        "Polymer",
        "React",
        "RxJS",
        "Vue.js",
    ];

    test("render correctly", () => {
        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS },
        });
        expect(!!wrapper.vm).toBeTruthy();
        expect(wrapper.exists()).toBeTruthy();
        expect(wrapper.attributes("data-oruga")).toBe("autocomplete");
        expect(wrapper.html()).toMatchSnapshot();
    });

    test("has configurable menu and item tags", () => {
        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS, menuTag: "ul", itemTag: "li" },
        });
        expect(wrapper.find("ul.o-dropdown__menu").exists()).toBeTruthy();
        expect(wrapper.find("li.o-dropdown__item").exists()).toBeTruthy();
    });

    test("has a dropdown menu hidden by default", () => {
        const wrapper = mount(OAutocomplete, { attachTo: document.body });
        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeFalsy();
    });

    test("can emit input, focus and blur events", async () => {
        const VALUE_TYPED = "test";

        vi.useFakeTimers(); // use fake timers for input debounce

        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS },
        });

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        // open menu
        await input.trigger("focus");
        expect(wrapper.emitted("focus")).toHaveLength(1);

        await input.setValue(VALUE_TYPED);
        await input.trigger("input");
        vi.runAllTimers(); // run debounce timers

        expect(wrapper.emitted("update:input")).toHaveLength(1);
        expect(wrapper.emitted("update:input")?.[0]).toContain(VALUE_TYPED);

        await input.trigger("blur");
        expect(wrapper.emitted("blur")).toBeDefined();

        vi.useRealTimers(); // restore real timers
    });

    test("can autocomplete with keydown", async () => {
        const VALUE_TYPED = "Ang";

        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS, openOnFocus: true, keepOpen: false },
            attachTo: document.body,
        });

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        // open menu
        await input.trigger("focus");
        await input.setValue(VALUE_TYPED);

        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeTruthy();

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

        expect(input.element.value).toBe(OPTIONS[0]);
        expect(wrapper.emitted("select")).toStrictEqual([[OPTIONS[0]]]);
        expect(wrapper.emitted("update:modelValue")).toStrictEqual([
            [OPTIONS[0]],
        ]);

        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeFalsy();
    });

    test("set values correct when two-way-binded", async () => {
        const wrapper = mount(OAutocomplete, {
            props: {
                options: OPTIONS,
                modelValue: OPTIONS[2],
                "onUpdate:modelValue": (modelValue) => {
                    wrapper.setProps({ modelValue });
                },
            },
        });
        await nextTick(); // await child component rendering

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeFalsy();

        const optionElements = wrapper.findAll('[data-oruga="dropdown-item"]');
        expect(optionElements).toHaveLength(OPTIONS.length);

        // check default selected value
        expect(input.element.value).toBe(OPTIONS[2]);
        expect(optionElements[2].classes("itemSelectedClass"));
        expect(optionElements[2].attributes("aria-selected")).toBeTruthy();

        // chenge selection
        wrapper.setProps({ modelValue: OPTIONS[0] });
        await nextTick();

        // check new selected value
        expect(input.element.value).toBe(OPTIONS[0]);
        expect(optionElements[0].classes("itemSelectedClass"));
        expect(optionElements[0].attributes("aria-selected")).toBeTruthy();
    });

    test("close dropdown on esc", async () => {
        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS, openOnFocus: true },
            attachTo: document.body,
        });

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        // open menu
        await input.trigger("focus");

        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeTruthy();

        await input.trigger("keydown", { key: "Escape" });

        expect(dropdown.isVisible()).toBeFalsy();
    });

    test("close dropdown on click outside", async () => {
        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS, openOnFocus: true },
            attachTo: document.body,
        });

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        // open menu
        await input.trigger("focus");
        await setTimeout(); // await event handler get set

        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeTruthy();

        // click outside
        window.dispatchEvent(new Event("click"));
        await nextTick(); // await dom update

        expect(dropdown.isVisible()).toBeFalsy();
    });

    test("open dropdown on down key click", async () => {
        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS },
            attachTo: document.body,
        });

        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeFalsy();

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        await input.trigger("focus");
        await input.trigger("keydown", { key: "Down" });

        expect(dropdown.isVisible()).toBeTruthy();
    });

    test("manages tab pressed as expected", async () => {
        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS, openOnFocus: true, keepFirst: true },
            attachTo: document.body,
        });

        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeFalsy();

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        await input.trigger("keydown", { key: "Tab" });
        expect(dropdown.isVisible()).toBeFalsy();

        await input.trigger("focus");

        await input.trigger("keydown", { key: "Tab" });
        expect(input.element.value).toBe("");
    });

    test("can openOnFocus and keepFirst", async () => {
        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS, openOnFocus: true, keepFirst: true },
            attachTo: document.body,
        });

        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeFalsy();

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        await input.trigger("focus");

        expect(dropdown.isVisible()).toBeTruthy();
    });

    test("keepFirst works as input changes", async () => {
        vi.useFakeTimers(); // use fake timers for input debounce

        const wrapper = mount(OAutocomplete, {
            props: {
                options: OPTIONS,
                openOnFocus: true,
                keepFirst: true,
            },
            attachTo: document.body,
        });

        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeFalsy();

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        await input.trigger("focus");

        expect(dropdown.isVisible()).toBeTruthy();

        const focusedItem = wrapper.find(".o-dropdown__item--focused");
        expect(focusedItem.exists()).toBeTruthy();
        expect(focusedItem.text()).toContain(OPTIONS[0]);

        await input.setValue("v");
        vi.runAllTimers(); // run debounce timers
        await nextTick();

        const updatedFocusedItem = wrapper.find(".o-dropdown__item--focused");
        expect(updatedFocusedItem.exists()).toBeTruthy();
        expect(updatedFocusedItem.text()).toContain("Vue.js");

        vi.useRealTimers(); // restore real timers
    });

    test("do not open when openOnFocus and empty options", async () => {
        const wrapper = mount(OAutocomplete, {
            props: { options: [], openOnFocus: true },
            attachTo: document.body,
        });

        const dropdown = wrapper.find(".o-dropdown__menu");
        expect(dropdown.exists()).toBeTruthy();
        expect(dropdown.isVisible()).toBeFalsy();

        const input = wrapper.find("input");
        expect(input.exists()).toBeTruthy();

        await input.trigger("focus");

        expect(dropdown.isVisible()).toBeFalsy();
    });

    test("reset events before destroy", async () => {
        const documentDummyListener = vi.fn();
        const windowDummyListener = vi.fn();
        document.removeEventListener = documentDummyListener;
        window.removeEventListener = windowDummyListener;

        const wrapper = mount(OAutocomplete, {
            props: { options: OPTIONS },
        });
        await nextTick(); // await event handler get set

        wrapper.unmount();

        expect(documentDummyListener).toHaveBeenCalledTimes(2);
        // remove scroll listener
        expect(documentDummyListener).toHaveBeenCalledWith(
            "scroll",
            expect.any(Function),
        );

        expect(windowDummyListener).toHaveBeenCalledTimes(2);
        // remove position listener
        expect(windowDummyListener).toHaveBeenCalledWith(
            "resize",
            expect.any(Function),
        );
    });

    describe("filtering", () => {
        test("do not sort when `backend-filtering` is given", async () => {
            vi.useFakeTimers();

            const wrapper = mount(OAutocomplete, {
                props: {
                    options: OPTIONS,
                    backendFiltering: true,
                    active: true,
                },
                attachTo: document.body,
            });
            await nextTick(); // await child component rendering

            const input = wrapper.find("input");
            expect(input.exists()).toBeTruthy();

            let optionElements = wrapper.findAll(
                '[data-oruga="dropdown-item"]',
            );
            expect(optionElements).toHaveLength(OPTIONS.length);
            optionElements.forEach((option) => {
                expect(option.attributes("disabled")).toBeUndefined();
            });

            await input.setValue(OPTIONS[2]);
            vi.runAllTimers(); // await debounce input handler

            await nextTick(); // await child component rendering

            // check that there are no out filtered elements
            optionElements = wrapper.findAll('[data-oruga="dropdown-item"]');
            expect(optionElements).toHaveLength(OPTIONS.length);
            optionElements.forEach((option) => {
                expect(option.isVisible()).toBeTruthy();
                expect(option.attributes("disabled")).toBeUndefined();
            });
            vi.useRealTimers();
        });

        test("show available options correctly when filter is given", async () => {
            vi.useFakeTimers();

            const wrapper = mount(OAutocomplete, {
                props: { options: OPTIONS, active: true },
                attachTo: document.body,
            });
            await nextTick(); // await child component rendering

            const input = wrapper.find("input");
            expect(input.exists()).toBeTruthy();

            let optionElements = wrapper.findAll(
                '[data-oruga="dropdown-item"]',
            );
            expect(optionElements).toHaveLength(OPTIONS.length);
            optionElements.forEach((option) => {
                expect(option.attributes("disabled")).toBeUndefined();
            });

            await input.setValue("j");
            vi.runAllTimers(); // await debounce input handler

            await nextTick(); // await child component rendering

            // check that there are no out filtered elements
            optionElements = wrapper.findAll('[data-oruga="dropdown-item"]');
            expect(optionElements).toHaveLength(OPTIONS.length);
            optionElements.forEach((option) => {
                expect(option.isVisible()).toBe(
                    option.text().toLowerCase().includes("j"),
                );
                expect(option.attributes("disabled")).toBeUndefined();
            });
            vi.useRealTimers();
        });
    });

    describe("clear button", () => {
        test("clear button does not exist when the search input is empty", () => {
            const wrapper = mount(OAutocomplete, {
                props: {
                    options: OPTIONS,
                    modelValue: "",
                    clearable: true,
                },
            });

            const subject = wrapper.find(".o-icon");
            expect(subject.exists()).toBeFalsy();
        });

        test("clears search input text when clear button gets clicked", async () => {
            const wrapper = mount(OAutocomplete, {
                props: {
                    options: OPTIONS,
                    modelValue: OPTIONS[5],
                    clearable: true,
                },
            });
            await nextTick(); // await child component rendering

            const input = wrapper.find("input");
            expect(input.exists()).toBeTruthy();
            expect(input.element.value).toBe(OPTIONS[5]);

            const icon = wrapper.find(".o-icon");
            expect(icon.exists()).toBeTruthy();
            await icon.trigger("click");

            expect(input.element.value).toEqual("");
        });

        test("clear button does not appear when clearable property is not set to true", () => {
            const wrapper = mount(OAutocomplete, {
                props: {
                    options: OPTIONS,
                    modelValue: OPTIONS[5],
                    clearable: false,
                },
            });

            const subject = wrapper.find(".o-icon");
            expect(subject.exists()).toBeFalsy();
        });
    });

    describe("header & footer", () => {
        test("can emit select-header by keyboard and click", async () => {
            const wrapper = mount(OAutocomplete, {
                props: {
                    openOnFocus: true,
                    keepOpen: true,
                    selectableHeader: true,
                    selectableFooter: true,
                },
                slots: {
                    header: "<h1>SLOT HEADER</h1>",
                    footer: "<h1>SLOT FOOTER</h1>",
                },
            });

            const input = wrapper.find("input");
            expect(input.exists()).toBeTruthy();

            // open menu
            await input.trigger("focus");

            // move to header and select by enter
            await input.trigger("keydown", { key: "Down" });
            await input.trigger("keydown", { key: "Enter" });

            expect(wrapper.emitted("select-header")).toHaveLength(1);

            const header = wrapper.find(".o-autocomplete__item-header");
            expect(header.exists()).toBeTruthy();
            await header.trigger("click");

            expect(wrapper.emitted("select-header")).toHaveLength(2);
        });

        test("can emit select-footer by keyboard and click", async () => {
            const wrapper = mount(OAutocomplete, {
                props: {
                    openOnFocus: true,
                    keepOpen: true,
                    selectableHeader: true,
                    selectableFooter: true,
                },
                slots: {
                    header: "<h1>SLOT HEADER</h1>",
                    footer: "<h1>SLOT FOOTER</h1>",
                },
            });
            const input = wrapper.find("input");

            // open menu
            await input.trigger("focus");

            // move to footer and select by enter
            await input.trigger("keydown", { key: "Down" });
            await input.trigger("keydown", { key: "Down" });
            await input.trigger("keydown", { key: "Enter" });

            expect(wrapper.emitted("select-footer")).toHaveLength(1);

            const footer = wrapper.find(".o-autocomplete__item-footer");
            expect(footer.exists()).toBeTruthy();
            await footer.trigger("click");

            expect(wrapper.emitted("select-footer")).toHaveLength(2);
        });
    });

    describe("render options props correctly", () => {
        test("handle options as primitves correctly", () => {
            const options: OptionsProp = ["Flint", "Silver", "Vane", 0, 1, 2];

            const wrapper = mount(OAutocomplete, { props: { options } });

            const optionElements = wrapper.findAll(
                '[data-oruga="dropdown-item"]',
            );
            expect(optionElements).toHaveLength(options.length);

            optionElements.forEach((el, idx) => {
                expect(el.text()).toBe(String(options[idx]));
                expect(el.attributes("aria-disabled")).toBe("false");
            });
        });

        test("handle options as object correctly", () => {
            const options: OptionsProp = {
                flint: "Flint",
                silver: "Silver",
                vane: "Vane",
                0: "Zero",
                1: "One",
                2: "Two",
            };

            const wrapper = mount(OAutocomplete, { props: { options } });

            const optionElements = wrapper.findAll(
                '[data-oruga="dropdown-item"]',
            );
            expect(optionElements).toHaveLength(Object.keys(options).length);

            optionElements.forEach((el, idx) => {
                expect(el.text()).toBe(Object.entries(options)[idx][1]);
                expect(el.attributes("aria-disabled")).toBe("false");
            });
        });

        test("handle options as options array correctly", () => {
            const options: AutocompleteOptions<string | number> = [
                { label: "Flint", value: "flint" },
                { label: "Silver", value: "silver", disabled: true },
                { label: "Vane", value: "vane" },
                { label: "Zero", value: 0 },
                { label: "One", value: 1 },
                { label: "Two", value: 2, disabled: true },
            ];

            const wrapper = mount(OAutocomplete, { props: { options } });

            const optionElements = wrapper.findAll(
                '[data-oruga="dropdown-item"]',
            );
            expect(optionElements).toHaveLength(options.length);

            optionElements.forEach((el, idx) => {
                expect(el.text()).toBe(options[idx].label);
                expect(el.attributes("aria-disabled")).toBe(
                    options[idx]?.disabled ? "true" : "false",
                );
            });
        });

        test("handle grouped options correctly", () => {
            const options: AutocompleteOptions<string | number | object> = [
                {
                    label: "Black Sails",
                    options: [
                        { label: "Flint", value: "flint" },
                        { label: "Silver", value: "silver" },
                        { label: "Vane", value: "vane" },
                        { label: "Billy", value: "billy" },
                    ],
                },
                {
                    label: "Breaking Bad",
                    options: {
                        heisenberg: "Heisenberg",
                        jesse: "Jesse",
                        saul: "Saul",
                        mike: "Mike",
                    },
                },
                {
                    label: "Game of Thrones",
                    disabled: true,
                    options: [
                        "Tyrion Lannister",
                        "Jamie Lannister",
                        "Daenerys Targaryen",
                        "Jon Snow",
                    ],
                },
            ];

            const wrapper = mount(OAutocomplete, { props: { options } });

            const optionElements = wrapper.findAll(
                '[data-oruga="dropdown-item"]',
            );
            expect(optionElements).toHaveLength(15);

            optionElements.forEach((el, idx) => {
                const isGroup = idx % 5 == 0;
                const g_idx = Math.floor(idx / 5);
                const o_idx = (idx % 5) - 1;

                if (isGroup) {
                    const option = options[g_idx];
                    expect(el.text()).toBe(option.label);
                    expect(el.attributes("aria-disabled")).toBe(
                        option.disabled ? "true" : "false",
                    );
                } else {
                    const g_options = options[g_idx].options;

                    let optionLabel;
                    if (idx < 5) {
                        optionLabel = (
                            g_options[o_idx] as DropdownItemProps<string>
                        ).label;
                    } else if (idx < 10) {
                        optionLabel = Object.entries(g_options)[o_idx][1];
                    } else {
                        optionLabel = g_options[o_idx];
                    }

                    expect(el.text()).toBe(optionLabel);
                }
            });
        });
    });
});
