import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { get } from "svelte/store";
import { editorStore } from "$lib/stores/editor";
import { setMockInvokeResult } from "../../../vitest.setup";

const FILE_CONTENT = "// test file content";

// Reset tabs between tests
afterEach(async () => {
  const tabs = get(editorStore.tabs);
  for (const tab of tabs) {
    editorStore.closeTab(tab.id);
  }
  editorStore.hideEditor();
});

describe("editorStore - openFile (tests getLanguageFromPath)", () => {
  const testCases: [string, string][] = [
    ["file.ts", "typescript"],
    ["file.tsx", "typescript"],
    ["file.js", "javascript"],
    ["file.jsx", "javascript"],
    ["file.py", "python"],
    ["file.rs", "rust"],
    ["file.go", "go"],
    ["file.java", "java"],
    ["file.c", "c"],
    ["file.cpp", "cpp"],
    ["file.h", "c"],
    ["file.hpp", "cpp"],
    ["file.cs", "csharp"],
    ["file.rb", "ruby"],
    ["file.php", "php"],
    ["file.swift", "swift"],
    ["file.kt", "kotlin"],
    ["file.scala", "scala"],
    ["file.html", "html"],
    ["file.htm", "html"],
    ["file.css", "css"],
    ["file.scss", "scss"],
    ["file.sass", "sass"],
    ["file.less", "less"],
    ["file.json", "json"],
    ["file.xml", "xml"],
    ["file.yaml", "yaml"],
    ["file.yml", "yaml"],
    ["file.toml", "toml"],
    ["file.md", "markdown"],
    ["file.markdown", "markdown"],
    ["file.sql", "sql"],
    ["file.sh", "shell"],
    ["file.bash", "shell"],
    ["file.zsh", "shell"],
    ["file.ps1", "powershell"],
    ["file.svelte", "svelte"],
    ["file.vue", "vue"],
    ["file.graphql", "graphql"],
    ["file.gql", "graphql"],
    ["file.lua", "lua"],
    ["file.r", "r"],
    ["file.dart", "dart"],
    ["file.elm", "elm"],
    ["file.ex", "elixir"],
    ["file.exs", "elixir"],
    ["file.erl", "erlang"],
    ["file.hs", "haskell"],
    ["file.clj", "clojure"],
    ["file.lisp", "lisp"],
    ["file.ml", "ocaml"],
    ["file.fs", "fsharp"],
    ["file.zig", "zig"],
    ["file.nim", "nim"],
    ["file.v", "v"],
    ["file.wasm", "wasm"],
    ["file.wat", "wasm"],
    ["dockerfile", "plaintext"],
    ["file.unknown", "plaintext"],
    ["file", "plaintext"],
  ];

  it.each(testCases)("maps %s to language %s", async (filename, expectedLanguage) => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile(`/path/to/${filename}`);
    const activeTab = get(editorStore.activeTab);
    expect(activeTab?.language).toBe(expectedLanguage);
    if (activeTab) editorStore.closeTab(activeTab.id);
  });

  it("opens a new tab and makes it active", async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/file.ts");
    const tabs = get(editorStore.tabs);
    const activeTab = get(editorStore.activeTab);
    expect(tabs).toHaveLength(1);
    expect(activeTab?.fileName).toBe("file.ts");
    expect(activeTab?.content).toBe(FILE_CONTENT);
    expect(activeTab?.isDirty).toBe(false);
  });

  it("switches to existing tab instead of opening a duplicate", async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/file.ts");
    const firstTabId = get(editorStore.activeTabId);

    // Open another file to change active tab
    await editorStore.openFile("/path/to/other.ts");
    const secondTabId = get(editorStore.activeTabId);
    expect(secondTabId).not.toBe(firstTabId);

    // Re-open first file — should switch to existing tab
    await editorStore.openFile("/path/to/file.ts");
    expect(get(editorStore.activeTabId)).toBe(firstTabId);
    expect(get(editorStore.tabs)).toHaveLength(2);
  });

  it("handles open file errors gracefully", async () => {
    const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("read_file_content", new Error("File not found"));
    await editorStore.openFile("/path/to/missing.ts");
    expect(get(editorStore.tabs)).toHaveLength(0);
    expect(get(editorStore.saveError)).toContain("Failed to open file");
    consoleErrorSpy.mockRestore();
  });

  it("extracts filename from path correctly", async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/deep/nested/path/component.svelte");
    expect(get(editorStore.activeTab)?.fileName).toBe("component.svelte");
  });
});

describe("editorStore - saveFile", () => {
  beforeEach(async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/file.ts");
    // Make it dirty first
    editorStore.updateTabContent(get(editorStore.activeTabId)!, "modified content");
  });

  it("saves the active tab successfully", async () => {
    setMockInvokeResult("write_file_content", undefined);
    await editorStore.saveFile();
    expect(get(editorStore.activeTab)?.isDirty).toBe(false);
    expect(get(editorStore.activeTab)?.originalContent).toBe("modified content");
  });

  it("saves a specific tab by ID", async () => {
    setMockInvokeResult("write_file_content", undefined);
    const tabId = get(editorStore.activeTabId)!;
    await editorStore.saveFile(tabId);
    expect(get(editorStore.activeTab)?.isDirty).toBe(false);
  });

  it("does nothing when no tab ID matches", async () => {
    await editorStore.saveFile("non-existent-tab");
    // No error = pass
  });
});

describe("editorStore - saveFile error", () => {
  it("sets saveError when write fails", async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/file-err.ts");
    editorStore.updateTabContent(get(editorStore.activeTabId)!, "modified content");
    const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("write_file_content", new Error("Permission denied"));
    await editorStore.saveFile().catch(() => {});
    expect(get(editorStore.saveError)).toContain("Failed to save file");
    consoleErrorSpy.mockRestore();
  });
});

describe("editorStore - updateTabContent", () => {
  beforeEach(async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/file.ts");
  });

  it("updates content and marks tab as dirty", () => {
    const tabId = get(editorStore.activeTabId)!;
    editorStore.updateTabContent(tabId, "new content");
    const tab = get(editorStore.activeTab);
    expect(tab?.content).toBe("new content");
    expect(tab?.isDirty).toBe(true);
  });

  it("marks tab as clean when content matches original", () => {
    const tabId = get(editorStore.activeTabId)!;
    editorStore.updateTabContent(tabId, "modified");
    expect(get(editorStore.activeTab)?.isDirty).toBe(true);
    editorStore.updateTabContent(tabId, FILE_CONTENT);
    expect(get(editorStore.activeTab)?.isDirty).toBe(false);
  });

  it("hasDirtyTabs is true when any tab is dirty", () => {
    const tabId = get(editorStore.activeTabId)!;
    expect(get(editorStore.hasDirtyTabs)).toBe(false);
    editorStore.updateTabContent(tabId, "dirty content");
    expect(get(editorStore.hasDirtyTabs)).toBe(true);
  });
});

describe("editorStore - closeTab", () => {
  beforeEach(async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/file1.ts");
    await editorStore.openFile("/path/to/file2.ts");
  });

  it("removes the specified tab", () => {
    const tabs = get(editorStore.tabs);
    editorStore.closeTab(tabs[0].id);
    expect(get(editorStore.tabs)).toHaveLength(1);
    expect(get(editorStore.tabs)[0].filePath).toBe("/path/to/file2.ts");
  });

  it("sets active tab to remaining tab when active tab is closed", () => {
    const activeTabId = get(editorStore.activeTabId)!;
    editorStore.closeTab(activeTabId);
    expect(get(editorStore.activeTabId)).not.toBe(activeTabId);
    expect(get(editorStore.tabs)).toHaveLength(1);
  });
});

describe("editorStore - setActiveTab", () => {
  beforeEach(async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/file1.ts");
    await editorStore.openFile("/path/to/file2.ts");
  });

  it("sets the active tab to the specified ID", () => {
    const tabs = get(editorStore.tabs);
    editorStore.setActiveTab(tabs[0].id);
    expect(get(editorStore.activeTabId)).toBe(tabs[0].id);
  });
});

describe("editorStore - visibility controls", () => {
  it("showEditor sets isEditorVisible to true", () => {
    expect(get(editorStore.isEditorVisible)).toBe(false);
    editorStore.showEditor();
    expect(get(editorStore.isEditorVisible)).toBe(true);
  });

  it("hideEditor sets isEditorVisible to false", () => {
    editorStore.showEditor();
    editorStore.hideEditor();
    expect(get(editorStore.isEditorVisible)).toBe(false);
  });

  it("toggleEditor toggles isEditorVisible", () => {
    expect(get(editorStore.isEditorVisible)).toBe(false);
    editorStore.toggleEditor();
    expect(get(editorStore.isEditorVisible)).toBe(true);
    editorStore.toggleEditor();
    expect(get(editorStore.isEditorVisible)).toBe(false);
  });
});

describe("editorStore - toggleFileBrowser", () => {
  it("toggles the file browser visibility", () => {
    const initial = get(editorStore.isFileBrowserOpen);
    editorStore.toggleFileBrowser();
    expect(get(editorStore.isFileBrowserOpen)).toBe(!initial);
    editorStore.toggleFileBrowser();
    expect(get(editorStore.isFileBrowserOpen)).toBe(initial);
  });
});

describe("editorStore - file system operations", () => {
  it("createFile invokes backend and refreshes directory", async () => {
    setMockInvokeResult("create_file", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.createFile("/path/to", "new-file.ts");
    // No error = success
  });

  it("createFile handles errors gracefully", async () => {
    const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("create_file", new Error("Permission denied"));
    await editorStore.createFile("/path/to", "new-file.ts");
    expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to create file:", expect.any(Error));
    consoleErrorSpy.mockRestore();
  });

  it("createDirectory invokes backend and refreshes", async () => {
    setMockInvokeResult("create_directory", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.createDirectory("/path/to", "new-dir");
  });

  it("createDirectory handles errors gracefully", async () => {
    const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("create_directory", new Error("Failed"));
    await editorStore.createDirectory("/path/to", "new-dir");
    expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to create directory:", expect.any(Error));
    consoleErrorSpy.mockRestore();
  });

  it("deleteFile invokes backend and refreshes", async () => {
    setMockInvokeResult("delete_file", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.deleteFile("/path/to/file.ts");
  });

  it("deleteFile handles errors gracefully", async () => {
    const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("delete_file", new Error("Failed"));
    await editorStore.deleteFile("/path/to/file.ts");
    expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to delete file:", expect.any(Error));
    consoleErrorSpy.mockRestore();
  });

  it("deleteDirectory invokes backend and refreshes", async () => {
    setMockInvokeResult("delete_directory", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.deleteDirectory("/path/to/dir");
  });

  it("deleteDirectory handles errors gracefully", async () => {
    const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("delete_directory", new Error("Failed"));
    await editorStore.deleteDirectory("/path/to/dir");
    expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to delete directory:", expect.any(Error));
    consoleErrorSpy.mockRestore();
  });

  it("renamePath invokes backend and refreshes", async () => {
    setMockInvokeResult("rename_path", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.renamePath("/path/to/old-name.ts", "new-name.ts");
  });

  it("renamePath handles errors gracefully", async () => {
    const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("rename_path", new Error("Failed"));
    await editorStore.renamePath("/path/to/old-name.ts", "new-name.ts");
    expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to rename:", expect.any(Error));
    consoleErrorSpy.mockRestore();
  });
});

describe("editorStore - initializeFileTree", () => {
  it("loads directory entries and updates file tree", async () => {
    const entries = [
      { path: "/path/file.ts", name: "file.ts", isDirectory: false, children: undefined },
    ];
    setMockInvokeResult("list_directory", entries);
    await editorStore.initializeFileTree("/path");
    expect(get(editorStore.fileTree)).toEqual(entries);
    expect(get(editorStore.currentDirectory)).toBe("/path");
  });

  it("handles errors gracefully", async () => {
    const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    setMockInvokeResult("list_directory", new Error("Failed"));
    await editorStore.initializeFileTree("/path");
    expect(get(editorStore.isLoadingTree)).toBe(false);
    consoleErrorSpy.mockRestore();
  });
});

describe("editorStore - toggleDirectory", () => {
  beforeEach(async () => {
    const entries = [
      {
        path: "/path/dir",
        name: "dir",
        isDirectory: true,
        isExpanded: false,
        children: undefined,
      },
    ];
    setMockInvokeResult("list_directory", entries);
    await editorStore.initializeFileTree("/path");
  });

  it("does nothing when entry is not a directory", async () => {
    const nonDirEntry = {
      path: "/path/file.ts",
      name: "file.ts",
      isDirectory: false,
    };
    const treeBefore = get(editorStore.fileTree);
    await editorStore.toggleDirectory(nonDirEntry);
    expect(get(editorStore.fileTree)).toEqual(treeBefore);
  });

  it("expands a collapsed directory and loads children asynchronously", async () => {
    const children = [
      { path: "/path/dir/child.ts", name: "child.ts", isDirectory: false, children: undefined },
    ];
    setMockInvokeResult("list_directory", children);

    const dirEntry = get(editorStore.fileTree)[0];
    await editorStore.toggleDirectory(dirEntry);

    const updatedEntry = get(editorStore.fileTree)[0];
    expect(updatedEntry.isExpanded).toBe(true);
    expect(updatedEntry.children).toEqual(children);
    expect(updatedEntry.isLoading).toBe(false);
  });

  it("collapses an expanded directory", async () => {
    const children = [
      { path: "/path/dir/child.ts", name: "child.ts", isDirectory: false, children: undefined },
    ];
    setMockInvokeResult("list_directory", children);

    // Expand first
    const dirEntry = get(editorStore.fileTree)[0];
    await editorStore.toggleDirectory(dirEntry);
    expect(get(editorStore.fileTree)[0].isExpanded).toBe(true);

    // Now collapse
    const expandedEntry = get(editorStore.fileTree)[0];
    await editorStore.toggleDirectory(expandedEntry);
    expect(get(editorStore.fileTree)[0].isExpanded).toBe(false);
  });

  it("expands a directory that already has children without reloading", async () => {
    // Re-init with a directory that already has children
    const entries = [
      {
        path: "/path/dir",
        name: "dir",
        isDirectory: true,
        isExpanded: false,
        children: [
          {
            path: "/path/dir/child.ts",
            name: "child.ts",
            isDirectory: false,
            children: undefined,
          },
        ],
      },
    ];
    setMockInvokeResult("list_directory", entries);
    await editorStore.initializeFileTree("/path");

    // Toggle should expand without loading (children already present)
    const dirEntry = get(editorStore.fileTree)[0];
    await editorStore.toggleDirectory(dirEntry);

    const updatedEntry = get(editorStore.fileTree)[0];
    expect(updatedEntry.isExpanded).toBe(true);
    expect(updatedEntry.children).toHaveLength(1);
  });

  it("handles tree with multiple entries when toggling — exercises non-matching leaf fallback", async () => {
    // Tree has dir + a sibling file; toggling dir exercises the `return e` branch for the file
    const entries = [
      {
        path: "/path/dir",
        name: "dir",
        isDirectory: true,
        isExpanded: false,
        children: undefined,
      },
      { path: "/path/file.ts", name: "file.ts", isDirectory: false, children: undefined },
    ];
    setMockInvokeResult("list_directory", entries);
    await editorStore.initializeFileTree("/path");

    const children: never[] = [];
    setMockInvokeResult("list_directory", children);
    const dirEntry = get(editorStore.fileTree)[0];
    await editorStore.toggleDirectory(dirEntry);

    // The sibling file should remain unchanged
    expect(get(editorStore.fileTree)[1].path).toBe("/path/file.ts");
    expect(get(editorStore.fileTree)[0].isExpanded).toBe(true);
  });

  it("handles nested directory toggle — exercises recursive updateTree branches", async () => {
    // Tree: outer_dir → [inner_dir (no children), sibling_file]
    // Toggling inner_dir covers the recursive path through outer's children
    const entries = [
      {
        path: "/path/outer",
        name: "outer",
        isDirectory: true,
        isExpanded: true,
        children: [
          {
            path: "/path/outer/inner",
            name: "inner",
            isDirectory: true,
            isExpanded: false,
            children: undefined,
          },
          {
            path: "/path/outer/sibling.ts",
            name: "sibling.ts",
            isDirectory: false,
            children: undefined,
          },
        ],
      },
    ];
    setMockInvokeResult("list_directory", entries);
    await editorStore.initializeFileTree("/path");

    const innerChildren = [
      {
        path: "/path/outer/inner/deep.ts",
        name: "deep.ts",
        isDirectory: false,
        children: undefined,
      },
    ];
    setMockInvokeResult("list_directory", innerChildren);

    const innerEntry = get(editorStore.fileTree)[0].children![0];
    await editorStore.toggleDirectory(innerEntry);

    const outer = get(editorStore.fileTree)[0];
    expect(outer.children![0].isExpanded).toBe(true);
    expect(outer.children![0].children).toEqual(innerChildren);
  });
});

describe("editorStore - deleteFile closes open tab", () => {
  it("closes the tab when the deleted file is open", async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/file.ts");
    expect(get(editorStore.tabs)).toHaveLength(1);

    setMockInvokeResult("delete_file", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.deleteFile("/path/to/file.ts");
    expect(get(editorStore.tabs)).toHaveLength(0);
  });
});

describe("editorStore - deleteDirectory closes open tabs", () => {
  it("closes all tabs inside the deleted directory", async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/dir/file1.ts");
    await editorStore.openFile("/path/to/dir/nested/file2.ts");
    await editorStore.openFile("/path/to/other.ts");
    expect(get(editorStore.tabs)).toHaveLength(3);

    setMockInvokeResult("delete_directory", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.deleteDirectory("/path/to/dir");

    const remainingTabs = get(editorStore.tabs);
    expect(remainingTabs).toHaveLength(1);
    expect(remainingTabs[0].filePath).toBe("/path/to/other.ts");
  });
});

describe("editorStore - renamePath updates open tabs", () => {
  it("updates filePath and fileName when the renamed file is open in a tab", async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/old-name.ts");

    setMockInvokeResult("rename_path", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.renamePath("/path/to/old-name.ts", "new-name.ts");

    const tab = get(editorStore.activeTab);
    expect(tab?.filePath).toBe("/path/to/new-name.ts");
    expect(tab?.fileName).toBe("new-name.ts");
  });

  it("updates filePaths for tabs inside a renamed directory", async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/old-dir/file.ts");

    setMockInvokeResult("rename_path", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.renamePath("/path/to/old-dir", "new-dir");

    const tab = get(editorStore.activeTab);
    expect(tab?.filePath).toBe("/path/to/new-dir/file.ts");
  });

  it("leaves unrelated tabs unchanged when renaming — exercises return-t fallback", async () => {
    setMockInvokeResult("read_file_content", FILE_CONTENT);
    await editorStore.openFile("/path/to/unrelated.ts");
    await editorStore.openFile("/path/to/old-name.ts");

    setMockInvokeResult("rename_path", undefined);
    setMockInvokeResult("list_directory", []);
    await editorStore.renamePath("/path/to/old-name.ts", "new-name.ts");

    const tabs = get(editorStore.tabs);
    const unrelated = tabs.find((t) => t.filePath === "/path/to/unrelated.ts");
    expect(unrelated).toBeDefined();
    expect(unrelated?.fileName).toBe("unrelated.ts");
  });
});

describe("editorStore - refreshDirectory", () => {
  it("reloads the entire tree when refreshing the root directory", async () => {
    const initialEntries = [
      { path: "/root/file.ts", name: "file.ts", isDirectory: false, children: undefined },
    ];
    setMockInvokeResult("list_directory", initialEntries);
    await editorStore.initializeFileTree("/root");
    expect(get(editorStore.fileTree)).toHaveLength(1);

    const updatedEntries = [
      { path: "/root/file.ts", name: "file.ts", isDirectory: false, children: undefined },
      { path: "/root/new.ts", name: "new.ts", isDirectory: false, children: undefined },
    ];
    setMockInvokeResult("list_directory", updatedEntries);
    await editorStore.refreshDirectory("/root");
    expect(get(editorStore.fileTree)).toHaveLength(2);
  });

  it("updates a subdirectory entry in the tree", async () => {
    const initialEntries = [
      {
        path: "/root/subdir",
        name: "subdir",
        isDirectory: true,
        isExpanded: true,
        children: [],
      },
    ];
    setMockInvokeResult("list_directory", initialEntries);
    await editorStore.initializeFileTree("/root");

    const subChildren = [
      { path: "/root/subdir/file.ts", name: "file.ts", isDirectory: false, children: undefined },
    ];
    setMockInvokeResult("list_directory", subChildren);
    await editorStore.refreshDirectory("/root/subdir");

    const tree = get(editorStore.fileTree);
    expect(tree[0].children).toEqual(subChildren);
    expect(tree[0].isExpanded).toBe(true);
  });
});
