import {
    buildJsxTagHighlights,
    findJsonAttributeClose,
    parseAttributes,
    parseJsonAttribute,
    resolveAttributeValue,
} from "@src/lib/markdown/tree/html-attrs";
import { describe, expect, it } from "vitest";

describe("findJsonAttributeClose", () => {
    it("finds the closing brace for nested JSON objects and strings", () => {
        const json = '{"version": "2.0.0", "items": [{"id": 1}]}';
        const full = `{${json}}`;
        const bounds = findJsonAttributeClose(`${full} trailing`, 0);
        expect(bounds).toEqual({
            contentFrom: 1,
            contentTo: full.length - 1,
            closeBrace: full.length - 1,
        });
    });

    it("handles braces inside JSON strings", () => {
        const json = '{"note": "}"}';
        const full = `{${json}}`;
        const bounds = findJsonAttributeClose(full, 0);
        expect(bounds?.closeBrace).toBe(full.length - 1);
    });
});

describe("parseJsonAttribute", () => {
    it("parses JSON literals without evaluation", () => {
        expect(parseJsonAttribute("42")).toBe(42);
        expect(parseJsonAttribute('"hello"')).toBe("hello");
        expect(parseJsonAttribute('{"enabled": true}')).toEqual({ enabled: true });
    });

    it("returns undefined for invalid JSON", () => {
        expect(parseJsonAttribute("{ foo: 1 }")).toBeUndefined();
        expect(parseJsonAttribute("count")).toBeUndefined();
    });
});

describe("resolveAttributeValue", () => {
    it("resolves JSON attribute values", () => {
        expect(resolveAttributeValue({ type: "json", value: "100" })).toBe(100);
        expect(resolveAttributeValue({ type: "json", value: '"2.0.0"' })).toBe("2.0.0");
    });
});

describe("buildJsxTagHighlights", () => {
    const tag = '<Since v={"2.0.0"} t="true" b="false" />';

    it("covers the attribute section without gaps for the nested HTML parser", () => {
        const highlights = buildJsxTagHighlights(tag);
        const attrStart = tag.indexOf(" v=");
        const attrEnd = tag.lastIndexOf(">");

        const gaps = leftoverGaps(attrStart, attrEnd, highlights);
        expect(gaps).toEqual([]);
    });

    it("labels attribute names and quoted literals after a JSON value", () => {
        const highlights = buildJsxTagHighlights(tag);
        const names = textAtKinds(tag, highlights, "name");
        const strings = textAtKinds(tag, highlights, "string");

        expect(names).toEqual(["v", "t", "b"]);
        expect(strings).toEqual(['"true"', '"false"']);
    });

    it("matches parseAttributes JSON boundaries", () => {
        const { endIndex } = readTagNameForTest(tag);
        const attrString = tag.slice(endIndex, tag.lastIndexOf(">"));
        const attrs = parseAttributes(attrString);
        const expr = buildJsxTagHighlights(tag).find((h) => h.kind === "expression-content");

        expect(attrs.v).toEqual({ type: "json", value: '"2.0.0"' });
        expect(expr).toBeDefined();
        if (!expr) return;
        expect(tag.slice(expr.from, expr.to)).toBe('"2.0.0"');
        expect(attrs.t).toBe("true");
        expect(attrs.b).toBe("false");
    });
});

function readTagNameForTest(text: string): { endIndex: number } {
    let i = 1;
    while (i < text.length && !/[\s/>]/.test(text[i] ?? "")) i++;
    return { endIndex: i };
}

function leftoverGaps(
    from: number,
    to: number,
    spans: { from: number; to: number }[],
): { from: number; to: number }[] {
    const gaps: { from: number; to: number }[] = [];
    let pos = from;

    for (const span of spans) {
        if (span.from > pos) gaps.push({ from: pos, to: span.from });
        pos = Math.max(pos, span.to);
    }

    if (pos < to) gaps.push({ from: pos, to });
    return gaps;
}

function textAtKinds(
    text: string,
    highlights: ReturnType<typeof buildJsxTagHighlights>,
    kind: "name" | "string",
): string[] {
    return highlights.filter((h) => h.kind === kind).map((h) => text.slice(h.from, h.to));
}
