import { expect, test } from "@playwright/test";
import type { Locator, Page } from "@playwright/test";
import type { FASTListboxOption } from "../listbox-option/index.js";
import { fixtureURL } from "../__test__/helpers.js";
import type { FASTSelect } from "./select.js";

test.describe("Select", () => {
    let page: Page;
    let element: Locator;
    let root: Locator;

    test.beforeAll(async ({ browser }) => {
        page = await browser.newPage();

        element = page.locator("fast-select");

        root = page.locator("#root");

        await page.goto(fixtureURL("select--select"), {
            waitUntil: "load",
        });
    });

    test.afterAll(async () => {
        await page.close();
    });

    test("should have a role of `combobox`", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select></fast-select>
            `;
        });

        await expect(element).toHaveAttribute("role", "combobox");
    });

    test("should have a tabindex of 0 when `disabled` is not defined", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        await expect(element).toHaveAttribute("tabindex", "0");
    });

    test("should set the `aria-disabled` attribute equal to the `disabled` value", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select disabled></fast-select>
            `;
        });

        await expect(element).toHaveAttribute("aria-disabled", "true");

        await element.evaluate<void, FASTSelect>(node => {
            node.disabled = false;
        });

        await expect(element).toHaveAttribute("aria-disabled", "false");
    });

    test("should have the attribute aria-expanded set to false", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select></fast-select>
            `;
        });

        await expect(element).toHaveAttribute("aria-expanded", "false");
    });

    test("should set its value to the first enabled option", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        await expect(element).toHaveJSProperty("value", "Option 1");

        await expect(element).toHaveJSProperty("selectedIndex", 0);
    });

    test("should NOT have a tabindex when `disabled` is true", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select disabled></fast-select>
            `;
        });

        await expect(element).not.toHaveAttribute("tabindex");
    });

    test("should set its value to the first enabled option when disabled", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select disabled>
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        await expect(element).toHaveJSProperty("value", "Option 1");

        await expect(element).toHaveJSProperty("selectedIndex", 0);
    });

    test("should select the first option with a `selected` attribute", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option>Option 1</fast-option>
                    <fast-option selected>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        await expect(element).toHaveJSProperty("value", "Option 2");

        await expect(element).toHaveJSProperty("selectedIndex", 1);
    });

    test("should select the first option with a `selected` attribute when disabled", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select disabled>
                    <fast-option>Option 1</fast-option>
                    <fast-option selected>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        await expect(element).toHaveJSProperty("value", "Option 2");

        await expect(element).toHaveJSProperty("selectedIndex", 1);
    });

    test("should return the same value when the `value` property is set before connect", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select value="Option 2">
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        await expect(element).toHaveJSProperty("value", "Option 2");
    });

    test("should return the same value when the value property is set after connect", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        await element.evaluate<void, FASTSelect>(node => {
            node.value = "Option 3";
        });

        await expect(element).toHaveJSProperty("value", "Option 3");
    });

    test("should select the next selectable option when the value is set to match a disabled option", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option>Option 1</fast-option>
                    <fast-option disabled>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        await expect(element).toHaveJSProperty("value", "Option 1");

        await expect(element).toHaveJSProperty("selectedIndex", 0);

        await element.evaluate<void, FASTSelect>(node => {
            node.value = "Option 2";
        });

        await expect(element).toHaveJSProperty("value", "Option 3");

        await expect(element).toHaveJSProperty("selectedIndex", 2);
    });

    test("should update the `value` property when the selected option's `value` property changes", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        const options = element.locator("fast-option");

        await expect(element).toHaveJSProperty("value", "Option 1");

        await options.first().evaluate<void, FASTListboxOption>(node => {
            node.value = "new value";
        });

        await expect(element).toHaveJSProperty("value", "new value");
    });

    test("should return the `value` property as a string", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option value="1">Option 1</fast-option>
                    <fast-option value="2">Option 2</fast-option>
                    <fast-option value="3">Option 3</fast-option>
                </fast-select>
            `;
        });

        await expect(element).toHaveJSProperty("value", "1");

        expect(
            await element.evaluate<string, FASTSelect>(node => typeof node.value)
        ).toBe("string");
    });

    test("should update the aria-expanded attribute when opened", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        await element.evaluate(node =>
            Promise.all(node.getAnimations({ subtree: true }).map(a => a.finished))
        );

        element.click();

        await expect(element).toHaveAttribute("aria-expanded", "true");
    });

    test("should display the listbox when the `open` property is true before connecting", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select open>
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        const listbox = element.locator(".listbox");

        await expect(element).toHaveAttribute("open");

        await expect(listbox).toBeVisible();
    });

    ["input", "change"].forEach(eventName => {
        [
            { expectedValue: "Option 2", key: "ArrowDown" },
            { expectedValue: "Option 1", key: "ArrowUp" },
            { expectedValue: "Option 3", key: "End" },
            { expectedValue: "Option 1", key: "Home" },
        ].forEach(({ expectedValue, key }) => {
            test(`should NOT emit \`${eventName}\` event while open when the value changes by user input via ${key} key`, async () => {
                await root.evaluate(node => {
                    node.innerHTML = /* html */ `
                        <fast-select open>
                            <fast-option>Option 1</fast-option>
                            <fast-option>Option 2</fast-option>
                            <fast-option>Option 3</fast-option>
                        </fast-select>
                    `;
                });

                const [wasChanged] = await Promise.all([
                    element.evaluate(
                        (node, eventName) =>
                            Promise.race([
                                new Promise(resolve =>
                                    node.addEventListener(eventName, () =>
                                        resolve(eventName)
                                    )
                                ),
                                new Promise(resolve =>
                                    requestAnimationFrame(() =>
                                        setTimeout(() => resolve(false))
                                    )
                                ),
                            ]),
                        eventName
                    ),

                    element.evaluate((node, key) => {
                        node.dispatchEvent(new KeyboardEvent("keydown", { key }));
                    }, key),
                ]);

                expect(wasChanged).not.toBe(eventName);

                await expect(element).toHaveJSProperty("value", expectedValue);
            });

            test(`should emit \`${eventName}\` event while closed when the value changes by user input via ${key} key`, async () => {
                await root.evaluate(node => {
                    node.innerHTML = /* html */ `
                        <fast-select>
                            <fast-option>Option 1</fast-option>
                            <fast-option>Option 2</fast-option>
                            <fast-option>Option 3</fast-option>
                        </fast-select>
                    `;
                });

                const [wasChanged] = await Promise.all([
                    element.evaluate((node, eventName) => {
                        return new Promise(resolve => {
                            node.addEventListener(eventName, () => resolve(eventName));
                        });
                    }, eventName),

                    element.evaluate((node, key) => {
                        node.dispatchEvent(new KeyboardEvent("keydown", { key }));
                    }, key),
                ]);

                expect(wasChanged).toBe(eventName);

                await expect(element).toHaveJSProperty("value", expectedValue);
            });
        });
    });

    test.describe("when the value changes by programmatic interaction", () => {
        ["input", "change"].forEach(eventName => {
            test(`should NOT emit \`${eventName}\` event`, async () => {
                await page.goto(fixtureURL("select--select"));

                const [wasChanged] = await Promise.all([
                    element.evaluate(
                        (node, eventName) =>
                            Promise.race([
                                new Promise(resolve =>
                                    node.addEventListener(eventName, () =>
                                        resolve(eventName)
                                    )
                                ),
                                new Promise(resolve =>
                                    requestAnimationFrame(() =>
                                        setTimeout(() => resolve(false))
                                    )
                                ),
                            ]),
                        eventName
                    ),

                    element.evaluate<void, FASTSelect>(node => {
                        node.value = "Tom Baker";
                    }),
                ]);

                expect(wasChanged).not.toBe(eventName);
            });
        });
    });

    test.describe("when the owning form's reset() function is invoked", () => {
        test("should reset the value property to the first available option", async () => {
            await root.evaluate(node => {
                node.innerHTML = /* html */ `
                    <form>
                        <fast-select>
                            <fast-option>Option 1</fast-option>
                            <fast-option>Option 2</fast-option>
                            <fast-option>Option 3</fast-option>
                        </fast-select>
                    </form>
                `;
            });

            const form = page.locator("form");

            await expect(element).toHaveJSProperty("value", "Option 1");

            await element.evaluate<void, FASTSelect>(node => {
                node.value = "Option 2";
            });

            await expect(element).toHaveJSProperty("value", "Option 2");

            await form.evaluate((node: HTMLFormElement) => {
                node.reset();
            });

            await expect(element).toHaveJSProperty("value", "Option 1");
        });
    });

    test("should set the `aria-activedescendant` attribute to the ID of the currently selected option", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option id="option-1">Option 1</fast-option>
                    <fast-option id="option-2">Option 2</fast-option>
                    <fast-option id="option-3">Option 3</fast-option>
                </fast-select>
            `;
        });

        await expect(element).toHaveAttribute("aria-activedescendant", "option-1");

        await element.evaluate<void, FASTSelect>(node => {
            node.selectNextOption();
        });

        await expect(element).toHaveAttribute("aria-activedescendant", "option-2");

        await element.evaluate<void, FASTSelect>(node => {
            node.selectNextOption();
        });

        await expect(element).toHaveAttribute("aria-activedescendant", "option-3");
    });

    test("should set the `aria-controls` attribute to the ID of the internal listbox element while open", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        const listbox = element.locator(".listbox");

        const listboxId = await listbox.evaluate(node => node.id);

        await expect(element).toHaveAttribute("aria-controls", "");

        await element.evaluate<void, FASTSelect>(node => {
            node.open = true;
        });

        await expect(element).toHaveAttribute("aria-controls", listboxId);

        await element.evaluate<void, FASTSelect>(node => {
            node.open = false;
        });

        await expect(element).toHaveAttribute("aria-controls", "");
    });

    test("should update the `displayValue` when the selected option's content changes", async () => {
        await root.evaluate(node => {
            node.innerHTML = /* html */ `
                <fast-select>
                    <fast-option>Option 1</fast-option>
                    <fast-option>Option 2</fast-option>
                    <fast-option>Option 3</fast-option>
                </fast-select>
            `;
        });

        const options = element.locator("fast-option");

        await expect(element).toHaveJSProperty("displayValue", "Option 1");

        options.first().evaluate(node => {
            node.innerHTML = "innerHTML value";
        });

        await expect(element).toHaveJSProperty("displayValue", "innerHTML value");

        options.first().evaluate<void, FASTSelect>(node => {
            node.innerText = "innerText value";
        });

        await expect(element).toHaveJSProperty("displayValue", "innerText value");

        options.first().evaluate<void, FASTSelect>(node => {
            node.textContent = "textContent value";
        });

        await expect(element).toHaveJSProperty("displayValue", "textContent value");
    });
});
