import { EditorSelection } from "@codemirror/state";
import * as MarkdownStories from "@src/stories/playground/Markdown.stories";
import { composeStories } from "@storybook/react-vite";
import { describe, expect, it } from "vitest";
import {
    focusAndType,
    getEditorView,
    mountStory,
    nextFrame,
    readDoc,
    sleep,
    typeText,
} from "../../_setup/e2e-helpers";

const Stories = composeStories(MarkdownStories);

/** Query the visible slash menu's command items (rendered in a portal). */
function menuItems(): HTMLElement[] {
    return Array.from(document.querySelectorAll<HTMLElement>("[data-navigation-item-id]"));
}

/** Move the caret to a fresh empty line at the end of the document. */
async function caretOnFreshLine() {
    const view = await getEditorView();
    view.focus();
    const end = view.state.doc.length;
    view.dispatch({
        changes: { from: end, insert: "\n\n" },
        selection: EditorSelection.cursor(end + 2),
        scrollIntoView: true,
    });
    await nextFrame();
    await nextFrame();
    return view;
}

describe("MarkdownEditor / slash commands", () => {
    it("opens a filtered, keyboard-navigable menu and inserts boilerplate on Enter", async () => {
        mountStory(<Stories.Editor />);
        await caretOnFreshLine();

        await focusAndType("/head");
        await sleep(50);

        const items = menuItems();
        expect(items.length).toBeGreaterThan(0);
        // Filtering by "head" should only surface heading commands.
        expect(items.every((el) => el.dataset.navigationItemId?.startsWith("heading"))).toBe(true);

        // Navigate to the second heading and select it.
        await typeText("{ArrowDown}");
        await sleep(20);
        await typeText("{Enter}");
        await sleep(50);

        const doc = await readDoc();
        // The "/head" text is replaced by a heading prefix; no stray newline.
        expect(doc).toContain("## ");
        expect(doc).not.toContain("/head");
        expect(menuItems().length).toBe(0);
    });

    it("dismisses on Escape and leaves the slash as literal text", async () => {
        mountStory(<Stories.Editor />);
        await caretOnFreshLine();

        await focusAndType("/quote");
        await sleep(50);
        expect(menuItems().length).toBeGreaterThan(0);

        await typeText("{Escape}");
        await sleep(50);

        expect(menuItems().length).toBe(0);
        const doc = await readDoc();
        expect(doc).toContain("/quote");
    });

    it("highlights the slash and shows an Enter command placeholder until text is typed", async () => {
        mountStory(<Stories.Editor />);
        await caretOnFreshLine();

        await focusAndType("/");
        await sleep(50);

        const highlight = document.querySelector(".cm-md-slash-active");
        expect(highlight).not.toBeNull();
        const placeholder = document.querySelector(".cm-md-slash-placeholder");
        expect(placeholder?.textContent).toBe("Type to search");

        // Typing the query removes the placeholder but keeps the highlight.
        await typeText("head");
        await sleep(50);

        expect(document.querySelector(".cm-md-slash-placeholder")).toBeNull();
        const stillHighlighted = document.querySelector(".cm-md-slash-active");
        expect(stillHighlighted?.textContent).toBe("/head");
    });
});
