import { App, TFile, type CachedMetadata, type Editor, type EventRef, type MarkdownView } from "obsidian";
import { describe, expect, test, vi } from "vitest";
import EditorService from "../src/editorService";
import { createEditor } from "./mocks/pluginClassMocks";

type EventCallback = (...args: unknown[]) => unknown;
type TestViewMode = "preview" | "source" | "live-preview";

type Emitter = { on: ReturnType<typeof vi.fn>; emit: (name: string, ...args: unknown[]) => void; };

type ViewHarness = { rerender: ReturnType<typeof vi.fn>; view: MarkdownView; };

type Harness = {
    app: App;
    metadataEmitter: Emitter;
    registerEventHandle: (eventRef: EventRef) => void;
    registerEventSpy: ReturnType<typeof vi.fn>;
    setActiveFile: (path: string, cache?: CachedMetadata | null) => TFile;
    setEditor: (lines: string[]) => void;
    setViewMode: (mode: TestViewMode) => void;
    workspaceEmitter: Emitter;
    getActiveCache: () => CachedMetadata | null;
    getEditor: () => Editor;
    getRerenderSpy: () => ReturnType<typeof vi.fn>;
    getVaultContent: () => string;
};

function createEmitter(): Emitter {
    const handlers = new Map<string, EventCallback>();

    return {
        on: vi.fn((name: string, callback: EventCallback): EventRef => {
            handlers.set(name, callback);
            return { name, callback } as unknown as EventRef;
        }),
        emit: (name: string, ...args: unknown[]): void => {
            handlers.get(name)?.(...args);
        }
    };
}

function createFile(path: string): TFile {
    return Object.assign(new TFile(), { path });
}

function createView(editor: Editor, mode: TestViewMode): ViewHarness {
    const rerender = vi.fn();

    return {
        rerender,
        view: Object.assign({} as MarkdownView, {
            get editor(): Editor {
                return editor;
            },
            getState(): { mode: "preview" | "source"; source: boolean; } {
                if (mode === "preview") {
                    return { mode: "preview", source: false };
                }

                return { mode: "source", source: mode === "source" };
            },
            previewMode: { rerender }
        })
    };
}

function createHarness(initialMode: TestViewMode = "live-preview"): Harness {
    const app = new App();
    const workspaceEmitter = createEmitter();
    const metadataEmitter = createEmitter();
    const registerEventSpy = vi.fn((eventRef: EventRef): void => {
        void eventRef;
    });
    const registerEventHandle = (eventRef: EventRef): void => {
        registerEventSpy(eventRef);
    };

    let activeFile = createFile("notes/current.md");
    let activeCache: CachedMetadata | null = { headings: [] };
    let activeEditor = createEditor([ "# Current heading" ]);
    let activeViewMode = initialMode;
    let viewHarness = createView(activeEditor, activeViewMode);
    let vaultContent = activeEditor.getValue();

    app.workspace = {
        on: workspaceEmitter.on,
        getActiveFile: () => activeFile,
        getActiveViewOfType: () => viewHarness.view
    } as unknown as App["workspace"];
    app.metadataCache = {
        on: metadataEmitter.on,
        getFileCache: vi.fn((file: TFile) => {
            return file.path === activeFile.path ? activeCache : null;
        })
    } as unknown as App["metadataCache"];
    app.vault = {
        process: vi.fn(async (_file: TFile, update: (content: string) => string) => {
            vaultContent = update(vaultContent);
            return vaultContent;
        })
    } as unknown as App["vault"];

    return {
        app: app as App,
        metadataEmitter,
        registerEventHandle,
        registerEventSpy,
        setActiveFile: (path: string, cache: CachedMetadata | null = activeCache): TFile => {
            activeFile = createFile(path);
            activeCache = cache;
            return activeFile;
        },
        setEditor: (lines: string[]): void => {
            activeEditor = createEditor(lines);
            vaultContent = activeEditor.getValue();
            viewHarness = createView(activeEditor, activeViewMode);
        },
        setViewMode: (mode: TestViewMode): void => {
            activeViewMode = mode;
            viewHarness = createView(activeEditor, activeViewMode);
        },
        workspaceEmitter,
        getActiveCache: (): CachedMetadata | null => activeCache,
        getEditor: (): Editor => activeEditor,
        getRerenderSpy: (): ReturnType<typeof vi.fn> => viewHarness.rerender,
        getVaultContent: (): string => vaultContent
    };
}

describe("EditorService", () => {
    test("hydrates from the current workspace state during construction", () => {
        // Arrange
        const harness = createHarness();

        // Act
        const service = new EditorService(harness.app, harness.registerEventHandle, []);

        // Assert
        expect(service.file?.path).toBe("notes/current.md");
        expect(service.cache).toBe(harness.getActiveCache());
        expect(service.editor).toBe(harness.getEditor());
        expect(service.viewMode).toBe("live-preview");
        expect(harness.registerEventSpy).toHaveBeenCalledTimes(3);
    });

    test("runs built-in file-open synchronization before addon callbacks", () => {
        // Arrange
        const harness = createHarness();
        const nextCache: CachedMetadata = { headings: [] };
        const observed: Array<{ filePath: string | undefined; cache: CachedMetadata | null; }> = [];
        const service = new EditorService(harness.app, harness.registerEventHandle, [ {
            name: "file-open",
            callback: () => {
                observed.push({ filePath: service.file?.path, cache: service.cache });
            }
        } ]);

        const nextFile = harness.setActiveFile("notes/next.md", nextCache);

        // Act
        harness.workspaceEmitter.emit("file-open", nextFile);

        // Assert
        expect(observed).toEqual([ { filePath: "notes/next.md", cache: nextCache } ]);
    });

    test("registers metadata cache events on the metadata cache emitter", () => {
        // Arrange
        const harness = createHarness();
        const changedSpy = vi.fn();
        const changedCache: CachedMetadata = { headings: [] };
        const file = harness.setActiveFile("notes/changed.md", changedCache);

        new EditorService(harness.app, harness.registerEventHandle, [ { name: "changed", callback: changedSpy } ]);

        // Act
        harness.metadataEmitter.emit("changed", file, "# body", changedCache);

        // Assert
        expect(harness.metadataEmitter.on).toHaveBeenCalledWith("changed", expect.any(Function));
        expect(changedSpy).toHaveBeenCalledWith(file, "# body", changedCache);
    });

    test("syncState accepts an explicit metadata cache override", () => {
        // Arrange
        const harness = createHarness();
        const service = new EditorService(harness.app, harness.registerEventHandle, []);
        const overrideCache: CachedMetadata = { headings: [] };

        // Act
        service.syncState(overrideCache);

        // Assert
        expect(service.cache).toBe(overrideCache);
    });

    test("syncs state when the active leaf changes", () => {
        // Arrange
        const harness = createHarness();
        const service = new EditorService(harness.app, harness.registerEventHandle, []);
        const nextCache: CachedMetadata = { headings: [] };
        harness.setActiveFile("notes/other.md", nextCache);

        // Act
        harness.workspaceEmitter.emit("active-leaf-change", null);

        // Assert
        expect(service.file?.path).toBe("notes/other.md");
        expect(service.cache).toBe(nextCache);
    });

    test("syncs state when the workspace layout changes", () => {
        // Arrange
        const harness = createHarness();
        const service = new EditorService(harness.app, harness.registerEventHandle, []);
        const nextCache: CachedMetadata = { headings: [] };
        harness.setActiveFile("notes/layout.md", nextCache);

        // Act
        harness.workspaceEmitter.emit("layout-change");

        // Assert
        expect(service.file?.path).toBe("notes/layout.md");
        expect(service.cache).toBe(nextCache);
    });

    test("applyEditorChange skips vault writes outside preview mode", async () => {
        // Arrange
        const harness = createHarness("live-preview");
        const service = new EditorService(harness.app, harness.registerEventHandle, []);

        // Act
        await service.applyEditorChange({ line: 0, ch: 0 }, { line: 0, ch: 0 }, "Intro\n");

        // Assert
        expect(harness.getEditor().getValue()).toContain("Intro");
        expect(harness.getVaultContent()).not.toContain("Intro");
        expect(harness.getRerenderSpy()).not.toHaveBeenCalled();
    });

    test("applyEditorChange mirrors updates into the vault in preview mode", async () => {
        // Arrange
        const harness = createHarness("preview");
        const service = new EditorService(harness.app, harness.registerEventHandle, []);

        // Act
        await service.applyEditorChange({ line: 0, ch: 0 }, { line: 0, ch: 0 }, "Intro\n");

        // Assert
        expect(harness.getVaultContent()).toContain("Intro");
        expect(harness.getRerenderSpy()).toHaveBeenCalledWith(true);
    });
});
