import { EditorSelection, EditorState } from "@codemirror/state";
import { EditorView, runScopeHandlers } from "@codemirror/view";
import { Callout } from "@src/lib/markdown/editor/callout";
import { createMarkdownLanguage } from "@src/lib/markdown/editor/language";
import { markdownPairKeymap, markdownSnippetHandler } from "@src/lib/markdown/editor/snippets";
import { describe, expect, it } from "vitest";

function createView(doc: string, cursor?: number | { anchor: number; head: number }): EditorView {
    const state = EditorState.create({
        doc,
        extensions: [
            createMarkdownLanguage({ extensions: [Callout] }),
            markdownSnippetHandler,
            markdownPairKeymap,
        ],
        selection:
            typeof cursor === "number"
                ? EditorSelection.cursor(cursor)
                : cursor
                  ? EditorSelection.range(cursor.anchor, cursor.head)
                  : undefined,
    });

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

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

/** Dispatch a Backspace through the registered key handlers. */
function backspace(view: EditorView): boolean {
    return runScopeHandlers(view, new KeyboardEvent("keydown", { key: "Backspace" }), "editor");
}

/**
 * Simulate a single-character user input by walking the same input-handler
 * pipeline the editor uses. Falls back to a default insert when no handler
 * consumes the event so we exercise the trigger but otherwise behave like a
 * normal keystroke.
 */
function type(view: EditorView, text: string): void {
    const main = view.state.selection.main;
    const handlers = view.state.facet(EditorView.inputHandler);
    const buildTransaction = () => view.state.update(view.state.replaceSelection(text));
    for (const handler of handlers) {
        if (handler(view, main.from, main.to, text, buildTransaction)) {
            return;
        }
    }
    view.dispatch(buildTransaction());
}

describe("markdownSnippetHandler", () => {
    it("expands triple backtick into a fenced code block with caret on the empty content line", () => {
        const view = createView("``", 2);
        type(view, "`");

        expect(view.state.doc.toString()).toBe("```\n\n```");
        expect(view.state.selection.main.empty).toBe(true);
        // Caret on the empty middle line, after "```\n".
        expect(view.state.selection.main.from).toBe(4);

        view.destroy();
    });

    it("preserves leading indent when expanding a fence inside a nested context", () => {
        const view = createView("  ``", 4);
        type(view, "`");

        expect(view.state.doc.toString()).toBe("  ```\n  \n  ```");
        // Caret on the indented content line.
        expect(view.state.selection.main.from).toBe(8);

        view.destroy();
    });

    it("expands a triple backtick typed one keystroke at a time", () => {
        const view = createView("", 0);
        type(view, "`");
        type(view, "`");
        type(view, "`");

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

        view.destroy();
    });

    it("does not expand a fence from a single backtick after text", () => {
        const view = createView("abc", 3);
        type(view, "`");

        // A lone backtick must auto-pair, never clobber the preceding text.
        expect(view.state.doc.toString()).toBe("abc``");
        expect(view.state.selection.main.from).toBe(4);

        view.destroy();
    });

    it("does not expand a fence when the backticks are not at the line start", () => {
        const view = createView("x ``", 4);
        type(view, "`");

        expect(view.state.doc.toString()).toBe("x ```");

        view.destroy();
    });

    it("does not expand a fence when there is content after the cursor on the same line", () => {
        const view = createView("``rest", 2);
        type(view, "`");

        expect(view.state.doc.toString()).toBe("```rest");

        view.destroy();
    });

    it("does not expand a fence when typing inside an existing fenced code block", () => {
        const view = createView("```js\nlet x``\n```", 13);
        type(view, "`");

        expect(view.state.doc.toString()).toBe("```js\nlet x```\n```");

        view.destroy();
    });

    it("wraps a non-empty selection with backticks", () => {
        const view = createView("hello world", { anchor: 0, head: 5 });
        type(view, "`");

        expect(view.state.doc.toString()).toBe("`hello` world");
        expect(view.state.selection.main.from).toBe(1);
        expect(view.state.selection.main.to).toBe(6);

        view.destroy();
    });

    it("wraps a selection with asterisks for italic and again for bold", () => {
        const view = createView("hello world", { anchor: 0, head: 5 });
        type(view, "*");
        expect(view.state.doc.toString()).toBe("*hello* world");

        type(view, "*");
        expect(view.state.doc.toString()).toBe("**hello** world");

        view.destroy();
    });

    it("wraps a selection with tildes for strikethrough", () => {
        const view = createView("hello world", { anchor: 0, head: 5 });
        type(view, "~");
        type(view, "~");

        expect(view.state.doc.toString()).toBe("~~hello~~ world");

        view.destroy();
    });

    it("wraps a selection as a markdown link with caret between the parens", () => {
        const view = createView("hello world", { anchor: 0, head: 5 });
        type(view, "[");

        expect(view.state.doc.toString()).toBe("[hello]() world");
        expect(view.state.selection.main.empty).toBe(true);
        expect(view.state.selection.main.from).toBe(8);

        view.destroy();
    });

    it("does not wrap when the selection is inside a fenced code block", () => {
        const view = createView("```js\nlet x = 1\n```", { anchor: 6, head: 11 });
        type(view, "*");

        const doc = view.state.doc.toString();
        expect(doc).not.toContain("*let x *");
        expect(doc).not.toContain("*let x ");

        view.destroy();
    });

    it("auto-pairs a lone backtick for inline code", () => {
        const view = createView("run ", 4);
        type(view, "`");

        expect(view.state.doc.toString()).toBe("run ``");
        expect(view.state.selection.main.from).toBe(5);

        view.destroy();
    });

    it("auto-pairs and steps over brackets", () => {
        const view = createView("", 0);
        type(view, "(");
        expect(view.state.doc.toString()).toBe("()");
        expect(view.state.selection.main.from).toBe(1);

        type(view, ")");
        expect(view.state.doc.toString()).toBe("()");
        expect(view.state.selection.main.from).toBe(2);

        view.destroy();
    });

    it("does not double a closing bracket that was not auto-paired", () => {
        const view = createView("foo", 3);
        type(view, ")");

        expect(view.state.doc.toString()).toBe("foo)");

        view.destroy();
    });

    it("suppresses emphasis auto-pairing directly in front of a word", () => {
        const view = createView("word", 0);
        type(view, "*");

        expect(view.state.doc.toString()).toBe("*word");
        expect(view.state.selection.main.from).toBe(1);

        view.destroy();
    });

    it("deletes an empty auto-inserted pair with a single backspace", () => {
        const view = createView("", 0);
        type(view, "(");
        expect(view.state.doc.toString()).toBe("()");

        backspace(view);
        expect(view.state.doc.toString()).toBe("");
        expect(view.state.selection.main.from).toBe(0);

        view.destroy();
    });

    it("leaves a non-empty pair to the default backspace", () => {
        const view = createView("(x)", 2);
        // The pair handler declines (no empty pair around the caret), so the
        // default backspace binding is left to run.
        expect(backspace(view)).toBe(false);
        expect(view.state.doc.toString()).toBe("(x)");

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