import type { HeadingCache } from "obsidian";
import { describe, expect, test } from "vitest";
import { TocModel } from "../src/tocModel";
import type { FileKey, TocBlockItem, TocBlockModel } from "../src/types";
import { createTocModelPluginMock } from "./mocks/pluginClassMocks";
import {
    testHeadingsMixed,
    testHeadingsWithSpecialChars,
    testHeadingsWithoutFirstLevel,
    testOmitHeadingRegex,
    testStandardHeadings
} from "./mocks/testingObjects";

type ExpectedTocItem = { text: string; href: string; children: ExpectedTocItem[]; };

function testGetIndentationLevel(
    headingLevel: number,
    headingLevelStack: number[]
): { currentIndentLevel: number; headingLevelStack: number[]; } {
    // Pop from the stack until we find a heading level less than the current
    while (
        headingLevelStack.length > 0 // Avoid indentation for the first heading
        && headingLevel <= headingLevelStack[headingLevelStack.length - 1]
    ) {
        headingLevelStack.pop();
    }
    headingLevelStack.push(headingLevel);

    const currentIndentLevel = headingLevelStack.length - 1;

    return { currentIndentLevel, headingLevelStack };
}

function buildTocSource(headings: HeadingCache[]): string {
    let headingLevelStack: number[] = [];

    return headings
        .map((headingCache) => {
            const { currentIndentLevel, headingLevelStack: nextStack } = testGetIndentationLevel(
                headingCache.level,
                headingLevelStack
            );
            headingLevelStack = nextStack;

            return `${" ".repeat(currentIndentLevel * 4)}- ${headingCache.heading}`;
        })
        .join("\n");
}

function createModel(source: string, sourceFilePath: FileKey): TocBlockModel {
    const { app, settings, uiStateManager } = createTocModelPluginMock();

    return new TocModel(
        uiStateManager,
        app,
        settings,
        { localSettings: source, sourceFilePath },
        (key) => uiStateManager.getTocFoldState(key)
    )
        .model;
}

function toExpectedItems(items: TocBlockItem[]): ExpectedTocItem[] {
    return items.map((item) => ({ text: item.text, href: item.href, children: toExpectedItems(item.children) }));
}

function buildRenderedItems(headings: HeadingCache[], fileName: string): ExpectedTocItem[] {
    const filteredHeadings = headings.filter(({ heading }) => !testOmitHeadingRegex.test(heading));
    const source = buildTocSource(filteredHeadings);
    const model = createModel(source, `${fileName}.md`);

    return toExpectedItems(model.items);
}

describe("TocModel headings", () => {
    test("builds nested items from ordered heading levels", () => {
        // Arrange & Act
        const items = buildRenderedItems(testStandardHeadings, "testStandardHeadings");

        // Assert
        expect(items).toEqual([ {
            text: "Title 1 Level 1",
            href: "testStandardHeadings#Title 1 Level 1",
            children: [ {
                text: "Title 1 Level 2",
                href: "testStandardHeadings#Title 1 Level 2",
                children: [ {
                    text: "Title 1 Level 3",
                    href: "testStandardHeadings#Title 1 Level 3",
                    children: [ {
                        text: "Title 1 Level 4",
                        href: "testStandardHeadings#Title 1 Level 4",
                        children: [ {
                            text: "Title 1 Level 5",
                            href: "testStandardHeadings#Title 1 Level 5",
                            children: [ {
                                text: "Title 1 Level 6",
                                href: "testStandardHeadings#Title 1 Level 6",
                                children: []
                            } ]
                        } ]
                    } ]
                } ]
            } ]
        } ]);
    });

    test("builds nested items when the first heading is not level one", () => {
        // Arrange & Act
        const items = buildRenderedItems(testHeadingsWithoutFirstLevel, "testHeadingsWithoutFirstLevel");

        // Assert
        expect(items).toEqual([ {
            text: "Title 1 Level 2",
            href: "testHeadingsWithoutFirstLevel#Title 1 Level 2",
            children: [ {
                text: "Title 1 Level 3",
                href: "testHeadingsWithoutFirstLevel#Title 1 Level 3",
                children: [ {
                    text: "Title 1 Level 4",
                    href: "testHeadingsWithoutFirstLevel#Title 1 Level 4",
                    children: [ {
                        text: "Title 1 Level 5",
                        href: "testHeadingsWithoutFirstLevel#Title 1 Level 5",
                        children: [ {
                            text: "Title 1 Level 6",
                            href: "testHeadingsWithoutFirstLevel#Title 1 Level 6",
                            children: []
                        } ]
                    } ]
                } ]
            } ]
        } ]);
    });

    test("builds sibling branches from disorderly heading levels", () => {
        // Arrange & Act
        const items = buildRenderedItems(testHeadingsMixed, "testHeadingsMixed");

        // Assert
        expect(items).toEqual([ { text: "Title 1 Level 4", href: "testHeadingsMixed#Title 1 Level 4", children: [] }, {
            text: "Title 1 Level 1",
            href: "testHeadingsMixed#Title 1 Level 1",
            children: [ { text: "Title 1 Level 6", href: "testHeadingsMixed#Title 1 Level 6", children: [] }, {
                text: "Title 1 Level 2",
                href: "testHeadingsMixed#Title 1 Level 2",
                children: []
            }, {
                text: "Title 2 Level 2",
                href: "testHeadingsMixed#Title 2 Level 2",
                children: [ { text: "Title 1 Level 3", href: "testHeadingsMixed#Title 1 Level 3", children: [] } ]
            } ]
        } ]);
    });

    test("sanitizes rendered link text while preserving transformed href targets", () => {
        // Arrange & Act
        const items = buildRenderedItems(testHeadingsWithSpecialChars, "testHeadingsWithSpecialChars");

        // Assert
        expect(items).toEqual([ {
            text: "Title 1 level 1 with special chars, bold, italic, a-tag, highlighted and strikethrough text",
            href:
                "testHeadingsWithSpecialChars#Title 1 `level 1` {with special chars}, **bold**, _italic_, a-tag, ==highlighted== and ~~strikethrough~~ text",
            children: [ {
                text: "Title 1 level 2 with HTML",
                href: "testHeadingsWithSpecialChars#Title 1 level 2 <em style=\"color: black\">with HTML</em>",
                children: [ {
                    text: "Title 1 level 3 wikilink1 wikitext2 mdlink",
                    href:
                        "testHeadingsWithSpecialChars#Title 1 level 3 wikilink1 wikilink2 wikitext2 [mdlink](https://mdurl)",
                    children: [ {
                        text: "Title 1 level 4 wikilink1 wikitext2 mdlink1 wikilink3 wikitext3 mdlink2",
                        href:
                            "testHeadingsWithSpecialChars#Title 1 level 4 wikilink1 wikilink2 wikitext2 [mdlink1](https://mdurl) wikilink3 wikilink4 wikitext3 [mdlink2](https://mdurl)",
                        children: []
                    } ]
                } ]
            } ]
        } ]);
    });

    test("falls back to contentText when a markdown link alias is empty", () => {
        // Arrange & Act
        const model = createModel("- [](https://example.com)", "empty-markdown-alias.md");

        // Assert
        expect(toExpectedItems(model.items)).toEqual([ {
            text: "[](https://example.com)",
            href: "empty-markdown-alias#[](https://example.com)",
            children: []
        } ]);
    });
});
