import {
    gfmTableAddColumn,
    gfmTableAddRow,
    gfmTableRemoveColumn,
    gfmTableRemoveRow,
    locateGfmTableCells,
    parseGfmTable,
    serialiseGfmTable,
    type TableModel,
} from "@src/lib/markdown/tree/gfm-table";
import { describe, expect, it } from "vitest";

describe("GFM table", () => {
    it("parses simple GFM tables", () => {
        const source = "| a | b |\n| - | - |\n| 1 | 2 |";
        const model = parseGfmTable(source);
        expect(model).not.toBeNull();
        expect(model?.headers).toEqual(["a", "b"]);
        expect(model?.rows).toEqual([["1", "2"]]);
    });

    it("respects alignment markers", () => {
        const source = "| a | b | c |\n| :- | :-: | -: |\n| 1 | 2 | 3 |";
        const model = parseGfmTable(source);
        expect(model?.alignments).toEqual(["left", "center", "right"]);
    });

    it("round trips a table preserving content", () => {
        const source = "| Name | Score |\n| :--- | ----: |\n| Ari  | 12    |\n| Nova | 8     |";
        const model = parseGfmTable(source);
        if (!model) throw new Error("expected parse");
        const serialised = serialiseGfmTable(model);

        const reparsed = parseGfmTable(serialised);
        expect(reparsed).not.toBeNull();
        expect(reparsed?.headers).toEqual(model.headers);
        expect(reparsed?.alignments).toEqual(model.alignments);
        expect(reparsed?.rows).toEqual(model.rows);
    });

    it("returns null for non-table input", () => {
        expect(parseGfmTable("just a paragraph")).toBeNull();
    });
});

describe("locateGfmTableCells", () => {
    it("locates the content start offset of each cell", () => {
        const source = "| a | b |\n| - | - |\n| 1 | 2 |";
        const cells = locateGfmTableCells(source);
        expect(cells).not.toBeNull();
        // Header content starts: 'a' at 2, 'b' at 6.
        expect(cells?.headerOffsets).toEqual([2, 6]);
        expect(source[cells?.headerOffsets[0] ?? -1]).toBe("a");
        expect(source[cells?.headerOffsets[1] ?? -1]).toBe("b");
        // Body row content starts: '1' and '2'.
        expect(source[cells?.rowOffsets[0]?.[0] ?? -1]).toBe("1");
        expect(source[cells?.rowOffsets[0]?.[1] ?? -1]).toBe("2");
    });

    it("handles ragged padding and missing outer pipes", () => {
        const source = "Name | Score\n--- | ---\nAri  |   12";
        const cells = locateGfmTableCells(source);
        expect(cells).not.toBeNull();
        expect(source.slice(cells?.headerOffsets[0] ?? 0, (cells?.headerOffsets[0] ?? 0) + 4)).toBe(
            "Name",
        );
        expect(source.slice(cells?.headerOffsets[1] ?? 0, (cells?.headerOffsets[1] ?? 0) + 5)).toBe(
            "Score",
        );
        expect(source[cells?.rowOffsets[0]?.[1] ?? -1]).toBe("1");
    });

    it("returns null for non-table input", () => {
        expect(locateGfmTableCells("just a paragraph")).toBeNull();
    });
});

describe("GFM table edits", () => {
    const base: TableModel = {
        headers: ["a", "b"],
        alignments: ["default", "default"],
        rows: [["1", "2"]],
    };

    it("adds an empty trailing column", () => {
        const next = gfmTableAddColumn(base);
        expect(next.headers).toEqual(["a", "b", ""]);
        expect(next.alignments).toHaveLength(3);
        expect(next.rows).toEqual([["1", "2", ""]]);
    });

    it("removes the last column but keeps at least one", () => {
        const next = gfmTableRemoveColumn(base);
        expect(next.headers).toEqual(["a"]);
        expect(next.rows).toEqual([["1"]]);
        const single: TableModel = { headers: ["only"], alignments: ["default"], rows: [["x"]] };
        expect(gfmTableRemoveColumn(single)).toEqual(single);
    });

    it("adds an empty trailing row sized to the columns", () => {
        const next = gfmTableAddRow(base);
        expect(next.rows).toEqual([
            ["1", "2"],
            ["", ""],
        ]);
    });

    it("removes the last row and is a no-op with no rows", () => {
        expect(gfmTableRemoveRow(base).rows).toEqual([]);
        const empty: TableModel = { headers: ["a"], alignments: ["default"], rows: [] };
        expect(gfmTableRemoveRow(empty)).toEqual(empty);
    });

    it("round-trips edits through serialise/parse", () => {
        const widened = serialiseGfmTable(gfmTableAddColumn(base));
        const reparsed = parseGfmTable(widened);
        expect(reparsed?.headers).toEqual(["a", "b", ""]);
    });
});
