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 { indentMarkdownList } from "@src/lib/markdown/editor/list-indent";
import { afterEach, describe, expect, it } from "vitest";

let view: EditorView;

afterEach(() => {
    view?.destroy();
});

function createView(doc: string, cursor: number): EditorView {
    const state = EditorState.create({
        doc,
        selection: EditorSelection.cursor(cursor),
        extensions: [createMarkdownLanguage({ extensions: [Callout] })],
    });
    const parent = document.createElement("div");
    document.body.appendChild(parent);
    view = new EditorView({ state, parent });
    return view;
}

/** Index just after the marker + space of the line containing `needle`. */
function cursorInItem(doc: string, needle: string): number {
    return doc.indexOf(needle) + 1;
}

describe("indentMarkdownList", () => {
    it("indents an item under its preceding sibling", () => {
        const doc = "- one\n- two";
        const v = createView(doc, cursorInItem(doc, "two"));
        const handled = indentMarkdownList(v, false);
        expect(handled).toBe(true);
        expect(v.state.doc.toString()).toBe("- one\n  - two");
    });

    it("consumes Tab but does not change the first item (no preceding sibling)", () => {
        const doc = "- one\n- two";
        const v = createView(doc, cursorInItem(doc, "one"));
        const handled = indentMarkdownList(v, false);
        expect(handled).toBe(true);
        expect(v.state.doc.toString()).toBe(doc);
    });

    it("outdents a nested item to its parent level", () => {
        const doc = "- one\n  - two";
        const v = createView(doc, cursorInItem(doc, "two"));
        const handled = indentMarkdownList(v, true);
        expect(handled).toBe(true);
        expect(v.state.doc.toString()).toBe("- one\n- two");
    });

    it("consumes Shift-Tab but does not outdent a top-level item", () => {
        const doc = "- one\n- two";
        const v = createView(doc, cursorInItem(doc, "two"));
        const handled = indentMarkdownList(v, true);
        expect(handled).toBe(true);
        expect(v.state.doc.toString()).toBe(doc);
    });

    it("returns false outside a list", () => {
        const doc = "just a paragraph";
        const v = createView(doc, 4);
        expect(indentMarkdownList(v, false)).toBe(false);
        expect(indentMarkdownList(v, true)).toBe(false);
    });

    it("indents ordered list items by the marker width", () => {
        const doc = "1. one\n2. two";
        const v = createView(doc, cursorInItem(doc, "two"));
        const handled = indentMarkdownList(v, false);
        expect(handled).toBe(true);
        expect(v.state.doc.toString()).toBe("1. one\n   2. two");
    });

    it("indents an item together with its nested children", () => {
        const doc = "- one\n- two\n  - child";
        const v = createView(doc, cursorInItem(doc, "two"));
        const handled = indentMarkdownList(v, false);
        expect(handled).toBe(true);
        expect(v.state.doc.toString()).toBe("- one\n  - two\n    - child");
    });

    it("keeps the cursor with the indented text", () => {
        const doc = "- one\n- two";
        const start = doc.indexOf("two");
        const v = createView(doc, start);
        indentMarkdownList(v, false);
        // Two spaces inserted before the line shift the cursor right by two.
        expect(v.state.selection.main.head).toBe(start + 2);
    });
});
