import type { CachedMetadata, Editor, HeadingCache, Pos, SectionCache } from "obsidian";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { injectGlobals } from "../src/globals/globalFuncs";
import {
    // getLocalSettingsBulletTypeSuggestions,
    getLocalSettingsOmitSuggestions
} from "../src/settings/localSettingsCompletionOptions";
import { Validator } from "../src/validator";
import { createEditor, createPluginMock } from "./mocks/pluginClassMocks";

injectGlobals();

function createHeading(heading: string, level: number, line: number): HeadingCache {
    const position: Pos = {
        start: { line, col: 0, offset: 0 },
        end: { line, col: heading.length, offset: heading.length }
    };

    return { heading, level, position } as HeadingCache;
}

function createCodeSection(closingFenceLine: number): SectionCache {
    return {
        type: "code",
        position: { start: { line: 0, col: 0, offset: 0 }, end: { line: closingFenceLine, col: 3, offset: 0 } }
    } as SectionCache;
}

type ValidatorFixture = { metadata: CachedMetadata; editor: Editor; filePath: string; };

function createFixture(
    localYamlLines: string[],
    headings?: HeadingCache[],
    filePath = "folder/file-a.md"
): ValidatorFixture {
    const lines: string[] = [
        "```insta-toc",
        "---",
        ...localYamlLines,
        "---",
        "",
        "# Table of Contents",
        "",
        "- Existing",
        "```",
        "",
        "# Heading 1",
        "## Heading 2"
    ];

    const closingFenceLine = lines.indexOf("```", 1);
    const section = createCodeSection(closingFenceLine);

    const metadata: CachedMetadata = { sections: [ section ], headings };

    return { metadata, editor: createEditor(lines), filePath };
}

function getHeadingExcludePattern(validator: Validator): RegExp | undefined {
    const getter = Reflect.get(Validator.prototype as object, "getHeadingExcludePattern") as (
        this: Validator
    ) => RegExp | undefined;

    return getter.call(validator);
}

describe("Validator local settings behavior", () => {
    beforeEach(() => {
        vi.stubGlobal("window", {
            app: {
                plugins: {
                    getPlugin: vi.fn().mockReturnValue({
                        settings: { tocTitle: "Table of Contents", bulletType: "dash" }
                    })
                }
            }
        });
    });

    afterEach(() => {
        vi.unstubAllGlobals();
    });

    test("re-validates when local settings change but headings do not", () => {
        const stableHeadings: HeadingCache[] = [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ];

        const initial = createFixture([ "title:", "  name: Table of Contents" ], stableHeadings);
        const { plugin, setEditor } = createPluginMock(undefined, initial.editor);

        const validator = new Validator(plugin.editorService, plugin.settings, initial.metadata, initial.filePath);

        expect(validator.isValid()).toBe(true);
        expect(validator.fileHeadings.map((h) => h.heading)).toEqual([ "Heading 1", "Heading 2" ]);

        const updatedLocalConfigOnly = createFixture(
            [ "levels:", "  min: 2", "  max: 6", "omit:", "  - Heading 2" ],
            stableHeadings
        );

        setEditor(updatedLocalConfigOnly.editor);

        validator.update(
            plugin.editorService,
            plugin.settings,
            updatedLocalConfigOnly.metadata,
            updatedLocalConfigOnly.filePath
        );

        expect(validator.isValid()).toBe(true);
        expect(validator.localTocSettings.levels.min).toBe(2);
        expect(validator.fileHeadings).toEqual([]);
    });

    test("returns false when neither headings nor local config changed", () => {
        const headings: HeadingCache[] = [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ];

        const fixture = createFixture([ "title:", "  name: Table of Contents" ], headings);
        const { plugin } = createPluginMock(undefined, fixture.editor);

        const validator = new Validator(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath);

        expect(validator.isValid()).toBe(true);

        validator.update(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath);

        expect(validator.isValid()).toBe(false);
    });

    test("re-validates when forceRefresh is true even without content changes", () => {
        const fixture = createFixture([ "title:", "  name: Table of Contents" ], [
            createHeading("Heading 1", 1, 10),
            createHeading("Heading 2", 2, 11)
        ]);
        const { plugin } = createPluginMock(undefined, fixture.editor);

        const validator = new Validator(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath);

        expect(validator.isValid()).toBe(true);
        expect(validator.isValid(true)).toBe(true);
    });

    test("handles missing heading cache safely", () => {
        const noHeadingsFixture = createFixture([ "title:", "  name: Table of Contents" ], undefined);
        const { plugin } = createPluginMock(undefined, noHeadingsFixture.editor);

        const validator = new Validator(
            plugin.editorService,
            plugin.settings,
            noHeadingsFixture.metadata,
            noHeadingsFixture.filePath
        );

        expect(validator.isValid()).toBe(true);
        expect(validator.fileHeadings).toEqual([]);
    });

    test("resets local settings to global defaults when switching files", () => {
        const headings: HeadingCache[] = [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ];

        const firstFile = createFixture(
            [ "levels:", "  min: 2", "  max: 6", "omit:", "  - Heading 2" ],
            headings,
            "folder/file-a.md"
        );
        const { plugin, setEditor } = createPluginMock(undefined, firstFile.editor);

        const validator = new Validator(plugin.editorService, plugin.settings, firstFile.metadata, firstFile.filePath);

        expect(validator.isValid()).toBe(true);
        expect(validator.localTocSettings.levels.min).toBe(2);
        expect(validator.fileHeadings).toEqual([]);

        const secondFile = createFixture([], headings, "folder/file-b.md");

        setEditor(secondFile.editor);

        validator.update(plugin.editorService, plugin.settings, secondFile.metadata, secondFile.filePath);

        expect(validator.isValid()).toBe(true);
        expect(validator.localTocSettings.levels.min).toBeNull();
        expect(validator.localTocSettings.omit).toBeNull();
        expect(validator.fileHeadings.map((heading) => heading.heading)).toEqual([ "Heading 1", "Heading 2" ]);
    });

    test("restores previous local settings when applying invalid yaml", () => {
        const fixture = createFixture([ "levels:", "  min: 2", "  max: 6" ], [
            createHeading("Heading 1", 1, 10),
            createHeading("Heading 2", 2, 11)
        ]);
        const { plugin } = createPluginMock(undefined, fixture.editor);

        const validator = new Validator(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath);

        expect(validator.isValid()).toBe(true);
        expect(validator.localTocSettings.levels.min).toBe(2);

        const didApply = validator.applyLocalSettingsYaml("levels:\n  min: 9");

        expect(didApply).toBe(false);
        expect(validator.localTocSettings.levels.min).toBe(2);
        expect(validator.localTocSettings.levels.max).toBe(6);
    });

    test("reuses the compiled heading exclusion regex until exclusion inputs change", () => {
        // Arrange
        const fixture = createFixture([], [ createHeading("Heading 1", 1, 10), createHeading("Heading 2", 2, 11) ]);
        const { plugin } = createPluginMock({ excludedChars: [ "*" ] }, fixture.editor);
        const validator = new Validator(plugin.editorService, plugin.settings, fixture.metadata, fixture.filePath);

        expect(validator.isValid()).toBe(true);

        // Act
        const firstPattern = getHeadingExcludePattern(validator);
        const secondPattern = getHeadingExcludePattern(validator);

        validator.applyLocalSettingsYaml("exclude: _");
        const localChangePattern = getHeadingExcludePattern(validator);

        validator.update(
            plugin.editorService,
            { ...plugin.settings, excludedChars: [ "*", "#" ] },
            fixture.metadata,
            fixture.filePath
        );
        const globalChangePattern = getHeadingExcludePattern(validator);

        // Assert
        expect(firstPattern).toBeInstanceOf(RegExp);
        expect(secondPattern).toBe(firstPattern);
        expect(localChangePattern).toBeInstanceOf(RegExp);
        expect(localChangePattern).not.toBe(firstPattern);
        expect(globalChangePattern).toBeInstanceOf(RegExp);
        expect(globalChangePattern).not.toBe(localChangePattern);
    });

    // test("provides list type suggestions for local settings completions", () => {
    //     expect(getLocalSettingsBulletTypeSuggestions()).toEqual([
    //         "none",
    //         "armenian",
    //         "georgian",
    //         "lower-greek",
    //         "lower-latin",
    //         "upper-latin",
    //         "disc",
    //         "circle",
    //         "square",
    //         "decimal",
    //         "decimal-leading-zero",
    //         "lower-alpha",
    //         "upper-alpha",
    //         "lower-roman",
    //         "upper-roman"
    //     ]);
    // });

    test("builds omit suggestions from heading cache entries", () => {
        const headings: HeadingCache[] = [
            createHeading(" Heading 1 ", 1, 10),
            createHeading("Heading 1", 2, 11),
            createHeading("", 3, 12),
            createHeading("Heading 2 <!-- omit -->", 4, 13),
            createHeading("Heading 3", 5, 14)
        ];

        expect(getLocalSettingsOmitSuggestions(headings)).toEqual([ "Heading 1", "Heading 3" ]);
    });
});
