import type { CachedMetadata, Editor, HeadingCache, Pos, SectionCache } from "obsidian";
import { describe, expect, test, vi } from "vitest";

vi.mock("obsidian-dev-utils/obsidian/plugin/plugin-base", () => ({
    PluginBase: class PluginBase {
        public app: unknown;
        public manifest: unknown;

        public constructor(app?: unknown, manifest?: unknown) {
            this.app = app;
            this.manifest = manifest;
        }

        public registerEvent(): void {}
        public registerMarkdownCodeBlockProcessor(): void {}
        public addCommand(): void {}
        public addRibbonIcon(): void {}
        public async onloadImpl(): Promise<void> {}
        public async onunloadImpl(): Promise<void> {}
    }
}));

vi.mock("obsidian-dev-utils/obsidian/plugin/plugin-context", () => ({ initPluginContext: vi.fn() }));

vi.mock("../src/settings/PluginSettingManager", () => ({ PluginSettingsManager: class PluginSettingsManager {} }));

vi.mock("../src/settings/SettingsTab", () => ({ SettingTab: class SettingTab {} }));

vi.mock(
    "../src/svelte",
    () => ({
        CodeBlockComponent: class CodeBlockComponent {},
        LocalSettingsModal: class LocalSettingsModal {
            public async open(): Promise<void> {}
        },
        MarkdownComponentMounter: class MarkdownComponentMounter {}
    })
);

import { ManageToc } from "../src/ManageToc";
import InstaTocPlugin from "../src/Plugin";
import type EditorService from "../src/editorService";
import type { InstaTocSettings } from "../src/settings/Settings";
import { Validator } from "../src/validator";
import { createEditor, createPluginMock } from "./mocks/pluginClassMocks";

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

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;
}

function createFixture(headings: HeadingCache[], filePath = "folder/file-a.md"): ReloadFixture {
    const lines = [
        "```insta-toc",
        "---",
        "title:",
        "  name: Table of Contents",
        "---",
        "",
        "# Table of Contents",
        "",
        "- Existing",
        "```",
        "",
        "# Heading 1",
        "## Heading 2"
    ];
    const closingFenceLine = lines.indexOf("```", 1);

    return {
        filePath,
        editor: createEditor(lines),
        metadata: { sections: [ createCodeSection(closingFenceLine) ], headings }
    };
}

function createPluginUnderTest(
    settings: InstaTocSettings,
    editorService: EditorService,
    validator?: Validator
): InstaTocPlugin {
    const plugin = Object.create(InstaTocPlugin.prototype) as InstaTocPlugin;

    Object.defineProperty(plugin, "settings", { configurable: true, get: (): InstaTocSettings => settings });

    Reflect.set(plugin as object, "_editorService", editorService);
    Reflect.set(plugin as object, "_validator", validator);

    return plugin;
}

describe("InstaTocPlugin.reload", () => {
    test("returns undefined when editor service state is incomplete", async () => {
        // Arrange
        const pluginMock = createPluginMock();
        const plugin = createPluginUnderTest(pluginMock.plugin.settings, pluginMock.plugin.editorService);
        const manageSpy = vi.spyOn(ManageToc, "run").mockResolvedValue();
        const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);

        // Act
        const result = await plugin.reload();

        // Assert
        expect(result).toBeUndefined();
        expect(manageSpy).not.toHaveBeenCalled();

        consoleSpy.mockRestore();
        manageSpy.mockRestore();
    });

    test("creates validator state and runs ManageToc from the unified reload path", async () => {
        // Arrange
        const fixture = createFixture([ createHeading("Heading 1", 1, 11), createHeading("Heading 2", 2, 12) ]);
        const pluginMock = createPluginMock(undefined, fixture.editor);
        pluginMock.setActiveFilePath(fixture.filePath);
        pluginMock.setMetadataCache(fixture.metadata);
        const plugin = createPluginUnderTest(pluginMock.plugin.settings, pluginMock.plugin.editorService);
        const manageSpy = vi.spyOn(ManageToc, "run").mockResolvedValue();

        // Act
        const result = await plugin.reload({ forceValidate: true });

        // Assert
        expect(result?.activeFile.path).toBe(fixture.filePath);
        expect(result?.isValid).toBe(true);
        expect(plugin.validator).toBeInstanceOf(Validator);
        expect(manageSpy).toHaveBeenCalledWith(plugin.editorService, plugin.settings, plugin.validator);

        manageSpy.mockRestore();
    });

    test("updates the existing validator and honors cache overrides without managing the toc", async () => {
        // Arrange
        const initialFixture = createFixture([ createHeading("Heading 1", 1, 11), createHeading("Heading 2", 2, 12) ]);
        const nextCache: CachedMetadata = {
            sections: initialFixture.metadata.sections,
            headings: [ createHeading("Heading 1", 1, 11), createHeading("Heading 3", 2, 12) ]
        };
        const pluginMock = createPluginMock(undefined, initialFixture.editor);
        pluginMock.setActiveFilePath(initialFixture.filePath);
        pluginMock.setMetadataCache(initialFixture.metadata);
        const plugin = createPluginUnderTest(pluginMock.plugin.settings, pluginMock.plugin.editorService);
        const manageSpy = vi.spyOn(ManageToc, "run").mockResolvedValue();

        await plugin.reload({ forceValidate: true, manageToc: false });
        const validator = plugin.validator;
        const updateSpy = vi.spyOn(validator, "update");

        // Act
        const result = await plugin.reload({ cache: nextCache, forceValidate: true, manageToc: false });

        // Assert
        expect(pluginMock.plugin.editorService.syncState).toHaveBeenLastCalledWith(nextCache);
        expect(updateSpy).toHaveBeenCalledWith(
            plugin.editorService,
            plugin.settings,
            nextCache,
            initialFixture.filePath
        );
        expect(result?.validator).toBe(validator);
        expect(result?.isValid).toBe(true);
        expect(manageSpy).not.toHaveBeenCalled();

        updateSpy.mockRestore();
        manageSpy.mockRestore();
    });
});
