import { EditorSelection, EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { Callout } from "@src/lib/markdown/editor/callout";
import { createMarkdownLanguage } from "@src/lib/markdown/editor/language";
import { DEFAULT_SLASH_COMMANDS } from "@src/lib/markdown/editor/slash/commands";
import { slashDecorationsField } from "@src/lib/markdown/editor/slash/decorations";
import { getSlashSession, slashSessionField } from "@src/lib/markdown/editor/slash/field";
import { describe, expect, it } from "vitest";

function createView(doc: string, cursor = doc.length): EditorView {
    const state = EditorState.create({
        doc,
        extensions: [
            createMarkdownLanguage({ extensions: [Callout] }),
            slashSessionField,
            slashDecorationsField,
        ],
        selection: EditorSelection.cursor(cursor),
    });

    const parent = document.createElement("div");
    document.body.appendChild(parent);

    return new EditorView({ state, parent });
}

/** Simulate typing `text` one character at a time as `input.type` events. */
function type(view: EditorView, text: string): void {
    for (const char of text) {
        const pos = view.state.selection.main.head;
        view.dispatch({
            changes: { from: pos, insert: char },
            selection: EditorSelection.cursor(pos + 1),
            userEvent: "input.type",
        });
    }
}

function command(id: string) {
    const found = DEFAULT_SLASH_COMMANDS.find((c) => c.id === id);
    if (!found) throw new Error(`unknown command ${id}`);
    return found;
}

describe("slashSessionField", () => {
    it("opens a session when typing / at the start of a line", () => {
        const view = createView("", 0);
        type(view, "/");

        expect(getSlashSession(view.state)).toEqual({ from: 0, query: "" });

        view.destroy();
    });

    it("opens a session when typing / after whitespace", () => {
        const view = createView("foo ", 4);
        type(view, "/");

        expect(getSlashSession(view.state)?.from).toBe(4);

        view.destroy();
    });

    it("does not open a session mid-word", () => {
        const view = createView("foo", 3);
        type(view, "/");

        expect(getSlashSession(view.state)).toBeNull();

        view.destroy();
    });

    it("does not open a session inside a fenced code block", () => {
        const view = createView("```\n\n```", 4);
        type(view, "/");

        expect(getSlashSession(view.state)).toBeNull();

        view.destroy();
    });

    it("tracks the query as the user continues typing", () => {
        const view = createView("", 0);
        type(view, "/head");

        expect(getSlashSession(view.state)?.query).toBe("head");

        view.destroy();
    });

    it("keeps the session open across a single space but closes on two", () => {
        const view = createView("", 0);
        type(view, "/a ");
        expect(getSlashSession(view.state)?.query).toBe("a ");

        type(view, " ");
        expect(getSlashSession(view.state)).toBeNull();

        view.destroy();
    });

    it("closes the session when the slash is deleted", () => {
        const view = createView("", 0);
        type(view, "/h");
        const slashPos = getSlashSession(view.state)?.from ?? -1;

        view.dispatch({
            changes: { from: slashPos, to: slashPos + 1 },
            userEvent: "delete",
        });

        expect(getSlashSession(view.state)).toBeNull();

        view.destroy();
    });

    it("closes the session when the caret moves before the slash", () => {
        const view = createView("", 0);
        type(view, "/h");

        view.dispatch({ selection: EditorSelection.cursor(0) });

        expect(getSlashSession(view.state)).toBeNull();

        view.destroy();
    });
});

describe("DEFAULT_SLASH_COMMANDS", () => {
    it("replaces the /query range with a heading prefix and positions the caret", () => {
        const view = createView("", 0);
        type(view, "/h1");
        const session = getSlashSession(view.state);
        expect(session).not.toBeNull();

        command("heading-1").run(view, {
            from: session?.from ?? 0,
            to: view.state.selection.main.head,
        });

        expect(view.state.doc.toString()).toBe("# ");
        expect(view.state.selection.main.head).toBe(2);
        // Replacing the slash text closes the session.
        expect(getSlashSession(view.state)).toBeNull();

        view.destroy();
    });

    it("inserts a fenced code block with the caret on the empty middle line", () => {
        const view = createView("", 0);
        type(view, "/code");

        command("code-block").run(view, {
            from: 0,
            to: view.state.selection.main.head,
        });

        expect(view.state.doc.toString()).toBe("```\n\n```");
        expect(view.state.selection.main.head).toBe(4);

        view.destroy();
    });
});

function collectDecorations(view: EditorView) {
    const set = view.state.field(slashDecorationsField);
    const marks: Array<{ from: number; to: number }> = [];
    let widgets = 0;

    set.between(0, view.state.doc.length, (from, to, deco) => {
        if (deco.spec.widget) {
            widgets += 1;
        } else {
            marks.push({ from, to });
        }
    });

    return { marks, widgets };
}

describe("slashDecorationsField", () => {
    it("emits no decorations when there is no active session", () => {
        const view = createView("hello", 5);

        expect(collectDecorations(view)).toEqual({ marks: [], widgets: 0 });

        view.destroy();
    });

    it("highlights the lone slash and shows the placeholder when the query is empty", () => {
        const view = createView("", 0);
        type(view, "/");

        const { marks, widgets } = collectDecorations(view);
        // Mark wraps the slash and the trailing placeholder widget (0..2).
        expect(marks).toEqual([{ from: 0, to: 2 }]);
        expect(widgets).toBe(1);

        view.destroy();
    });

    it("extends the highlight over the query and drops the placeholder once text is typed", () => {
        const view = createView("", 0);
        type(view, "/head");

        const { marks, widgets } = collectDecorations(view);
        // Mark spans the slash + "head" (positions 0..5).
        expect(marks).toEqual([{ from: 0, to: 5 }]);
        expect(widgets).toBe(0);

        view.destroy();
    });
});
