import { Group, MantineProvider, Paper, Text, v8CssVariablesResolver } from "@mantine/core";
import type { MarkdownComponents } from "@src/lib/markdown";
import { MarkdownEditor } from "@src/primitives/MarkdownEditor";
import { MANTINE_THEME } from "@src/theme/mantine";
import { describe, expect, it } from "vitest";
import { getEditorView, mountStory, nextFrame } from "../../_setup/e2e-helpers";

function Card({ label }: { label: string }) {
    return (
        <Paper
            withBorder
            p="xs"
            radius="sm"
            mb="xs"
        >
            <Group gap="xs">
                <Text size="sm">value =</Text>
                <Text size="sm">{label}</Text>
            </Group>
        </Paper>
    );
}

const inlineComponents: MarkdownComponents = {
    Card,
};

const blockComponents: MarkdownComponents = {
    Card: { component: Card, block: true },
};

const ONE_PER_LINE_DOC = `# Heading

<Card label="Alpha" />

<Card label="Beta" />

<Card label="Gamma" />
`;

const MANY_PER_LINE_DOC = `# Heading

<Card label="Alpha" /><Card label="Beta" /><Card label="Gamma" />
`;

function renderEditor(doc: string, components: MarkdownComponents) {
    return (
        <MantineProvider
            theme={MANTINE_THEME}
            cssVariablesResolver={v8CssVariablesResolver}
            forceColorScheme="dark"
        >
            <Paper
                p="md"
                withBorder
            >
                <MarkdownEditor
                    mode="preview"
                    document={doc}
                    autoFocus={false}
                    jsxMode="render"
                    components={components}
                />
            </Paper>
        </MantineProvider>
    );
}

function lineHeightsForInlineWidgets(): number[] {
    const lines = Array.from(document.querySelectorAll<HTMLElement>(".cm-line"));
    return lines
        .filter((line) => line.querySelector('[data-md-widget="JsxInlineWidget"]'))
        .map((line) => line.getBoundingClientRect().height);
}

/** Block replace widgets render between `.cm-line` rows, not inside them. */
function blockWidgetHostLine(widget: HTMLElement): HTMLElement | null {
    let el: HTMLElement | null = widget;
    while (el) {
        if (el.classList.contains("cm-line")) return el;
        el = el.parentElement;
    }
    return null;
}

async function mountPreviewEditor(doc: string, components: MarkdownComponents) {
    mountStory(renderEditor(doc, components));
    const view = await getEditorView();
    view.contentDOM.blur();
    await nextFrame();
    await nextFrame();
    return view;
}

describe("MarkdownEditor / inline JSX rendering block content", () => {
    it("does not stack cm-widgetBuffer images above and below the widget", async () => {
        mountStory(renderEditor(ONE_PER_LINE_DOC, inlineComponents));

        const view = await getEditorView();
        view.contentDOM.blur();
        await nextFrame();
        await nextFrame();

        const cardWidgets = document.querySelectorAll<HTMLElement>(
            '[data-md-widget="JsxInlineWidget"]',
        );
        expect(cardWidgets.length).toBe(3);

        const widgetHeights = Array.from(cardWidgets).map((w) => w.getBoundingClientRect().height);
        const lineHeights = lineHeightsForInlineWidgets();
        expect(lineHeights.length).toBe(3);

        for (let i = 0; i < lineHeights.length; i++) {
            const lineHeight = lineHeights[i] ?? 0;
            const widgetHeight = widgetHeights[i] ?? 0;
            expect(widgetHeight).toBeGreaterThan(0);
            expect(lineHeight).toBeGreaterThan(0);
            expect(lineHeight).toBeLessThan(widgetHeight + 20);
        }
    });

    it("keeps multiple inline JSX widgets on the same line side-by-side", async () => {
        mountStory(renderEditor(MANY_PER_LINE_DOC, inlineComponents));

        const view = await getEditorView();
        view.contentDOM.blur();
        await nextFrame();
        await nextFrame();

        const cardWidgets = document.querySelectorAll<HTMLElement>(
            '[data-md-widget="JsxInlineWidget"]',
        );
        expect(cardWidgets.length).toBe(3);

        const rects = Array.from(cardWidgets).map((w) => w.getBoundingClientRect());
        const firstTop = rects[0]?.top ?? 0;
        const tolerance = 4;
        for (const r of rects) {
            expect(Math.abs(r.top - firstTop)).toBeLessThan(tolerance);
        }
    });
});

describe("MarkdownEditor / block JSX components", () => {
    it("renders block components via JsxBlockWidget with an edit-source action", async () => {
        await mountPreviewEditor(ONE_PER_LINE_DOC, blockComponents);

        const blockWidgets = document.querySelectorAll<HTMLElement>(
            '[data-md-widget="JsxBlockWidget"]',
        );
        expect(blockWidgets.length).toBe(3);
        expect(document.querySelectorAll('[data-md-widget="JsxInlineWidget"]').length).toBe(0);

        const editButtons = document.querySelectorAll<HTMLButtonElement>(
            '[aria-label="Edit source"]',
        );
        expect(editButtons.length).toBeGreaterThanOrEqual(3);
    });

    it("renders block widgets between cm-lines, not inside a cm-line", async () => {
        await mountPreviewEditor(ONE_PER_LINE_DOC, blockComponents);

        expect(document.querySelectorAll(".jsx-block-line-host").length).toBe(0);

        for (const widget of Array.from(
            document.querySelectorAll<HTMLElement>('[data-md-widget="JsxBlockWidget"]'),
        )) {
            expect(blockWidgetHostLine(widget)).toBeNull();
            const lineSibling = widget.closest(".cm-line");
            expect(lineSibling).toBeNull();
        }
    });

    it("block widgets do not get cm-widgetBuffer spacer images", async () => {
        await mountPreviewEditor(ONE_PER_LINE_DOC, blockComponents);

        for (const widget of Array.from(
            document.querySelectorAll<HTMLElement>('[data-md-widget="JsxBlockWidget"]'),
        )) {
            const parent = widget.parentElement;
            expect(parent?.querySelector("img.cm-widgetBuffer")).toBeNull();
        }
    });

    it("clicking the edit-source action selects the component source range", async () => {
        const view = await mountPreviewEditor(ONE_PER_LINE_DOC, blockComponents);

        const editButton = document.querySelector<HTMLButtonElement>('[aria-label="Edit source"]');
        expect(editButton).toBeTruthy();
        editButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
        await nextFrame();
        await nextFrame();

        const selection = view.state.selection.main;
        const selectedSource = view.state.sliceDoc(selection.from, selection.to);
        expect(selectedSource.trim()).toBe('<Card label="Alpha" />');
    });

    it("renders block components embedded in a mixed paragraph on a dedicated row", async () => {
        const doc = 'Intro text <Card label="Mid" /> trailing text.';
        await mountPreviewEditor(doc, blockComponents);

        const blockWidgets = document.querySelectorAll<HTMLElement>(
            '[data-md-widget="JsxBlockWidget"]',
        );
        expect(blockWidgets.length).toBe(1);
        const midWidget = blockWidgets[0];
        expect(midWidget?.textContent).toContain("Mid");
        expect(midWidget && blockWidgetHostLine(midWidget)).toBeNull();

        const lines = Array.from(document.querySelectorAll<HTMLElement>(".cm-line"));
        expect(lines.some((line) => line.classList.contains("jsx-block-line-host"))).toBe(false);

        const introLine = lines.find((line) => line.textContent?.includes("Intro text"));
        const trailingLine = lines.find((line) => line.textContent?.includes("trailing text"));
        expect(introLine).toBeTruthy();
        expect(trailingLine).toBeTruthy();
        expect(introLine?.style.display).not.toBe("flex");
        expect(trailingLine?.style.display).not.toBe("flex");
    });

    it("renders block HTML embedded in a mixed paragraph on a dedicated row", async () => {
        const doc = "hello world `snippet` <div>test</div> foo bar";
        await mountPreviewEditor(doc, blockComponents);

        const blockWidgets = document.querySelectorAll<HTMLElement>(
            '[data-md-widget="JsxBlockWidget"]',
        );
        expect(blockWidgets.length).toBe(1);
        const divWidget = blockWidgets[0];
        expect(divWidget?.textContent).toContain("test");
        expect(divWidget && blockWidgetHostLine(divWidget)).toBeNull();

        const lines = Array.from(document.querySelectorAll<HTMLElement>(".cm-line"));
        const helloLine = lines.find((line) => line.textContent?.includes("hello world"));
        const fooLine = lines.find((line) => line.textContent?.includes("foo bar"));
        expect(helloLine).toBeTruthy();
        expect(fooLine).toBeTruthy();
        expect(helloLine?.classList.contains("jsx-block-line-host")).toBe(false);
        expect(fooLine?.classList.contains("jsx-block-line-host")).toBe(false);
    });
});
