import { describe, it, expect, vi, beforeEach } from "vitest";
import { get } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import { setMockInvokeResult } from "../../../vitest.setup";
import {
  projectContextStore,
  PROJECT_FILE_NAMES,
  PROJECT_TEMPLATES,
  PROJECT_CONTEXT_SYSTEM_ADDENDUM,
  injectTextStore,
  type ProjectFile,
  type ProjectScan,
} from "./projectContext";

describe("PROJECT_FILE_NAMES", () => {
  it("maps all five project file types", () => {
    expect(PROJECT_FILE_NAMES.PROJECT).toBe("PROJECT.md");
    expect(PROJECT_FILE_NAMES.REQUIREMENTS).toBe("REQUIREMENTS.md");
    expect(PROJECT_FILE_NAMES.ROADMAP).toBe("ROADMAP.md");
    expect(PROJECT_FILE_NAMES.STATE).toBe("STATE.md");
    expect(PROJECT_FILE_NAMES.CODEBASE).toBe("CODEBASE.md");
  });
});

describe("PROJECT_TEMPLATES", () => {
  const editableFiles: ProjectFile[] = ["PROJECT", "REQUIREMENTS", "ROADMAP", "STATE"];

  it.each(editableFiles)("returns a non-empty template for %s", (file) => {
    expect(PROJECT_TEMPLATES[file]).toBeTruthy();
    expect(PROJECT_TEMPLATES[file].length).toBeGreaterThan(0);
  });

  it("has an empty string template for CODEBASE (auto-generated)", () => {
    expect(PROJECT_TEMPLATES.CODEBASE).toBe("");
  });
});

describe("projectContextStore", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe("initial state", () => {
    it("has null contents for all files", () => {
      const state = get(projectContextStore.contents);
      expect(state.PROJECT).toBeNull();
      expect(state.REQUIREMENTS).toBeNull();
      expect(state.ROADMAP).toBeNull();
      expect(state.STATE).toBeNull();
      expect(state.CODEBASE).toBeNull();
    });

    it("has false isLoading for all files", () => {
      const state = get(projectContextStore.isLoading);
      expect(state.PROJECT).toBe(false);
      expect(state.REQUIREMENTS).toBe(false);
      expect(state.ROADMAP).toBe(false);
      expect(state.STATE).toBe(false);
      expect(state.CODEBASE).toBe(false);
    });

    it("has false isSaving for all files", () => {
      const state = get(projectContextStore.isSaving);
      expect(state.PROJECT).toBe(false);
      expect(state.REQUIREMENTS).toBe(false);
      expect(state.ROADMAP).toBe(false);
      expect(state.STATE).toBe(false);
      expect(state.CODEBASE).toBe(false);
    });

    it("has PROJECT as the default activeFile", () => {
      expect(get(projectContextStore.activeFile)).toBe("PROJECT");
    });

    it("has false isMappingCodebase initially", () => {
      expect(get(projectContextStore.isMappingCodebase)).toBe(false);
    });

    it("exposes all expected methods", () => {
      expect(typeof projectContextStore.loadFile).toBe("function");
      expect(typeof projectContextStore.saveFile).toBe("function");
      expect(typeof projectContextStore.loadAll).toBe("function");
      expect(typeof projectContextStore.setActiveFile).toBe("function");
      expect(typeof projectContextStore.getTemplate).toBe("function");
      expect(typeof projectContextStore.mapCodebase).toBe("function");
      expect(typeof projectContextStore.finishMapping).toBe("function");
    });
  });

  describe("loadFile", () => {
    it("calls read_file_content with the correct path", async () => {
      setMockInvokeResult("read_file_content", "# Project\n\nContent here");

      await projectContextStore.loadFile("PROJECT", "/home/naomi/myproject");

      expect(invoke).toHaveBeenCalledWith("read_file_content", {
        path: "/home/naomi/myproject/PROJECT.md",
      });
    });

    it("updates contents store with file content on success", async () => {
      const content = "# My Project\n\nDescription here";
      setMockInvokeResult("read_file_content", content);

      await projectContextStore.loadFile("PROJECT", "/home/naomi/myproject");

      expect(get(projectContextStore.contents).PROJECT).toBe(content);
    });

    it("sets content to null when file does not exist", async () => {
      const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
      setMockInvokeResult("read_file_content", new Error("File not found"));

      await projectContextStore.loadFile("REQUIREMENTS", "/home/naomi/myproject");

      expect(get(projectContextStore.contents).REQUIREMENTS).toBeNull();
      consoleSpy.mockRestore();
    });

    it("sets isLoading to false after completion", async () => {
      setMockInvokeResult("read_file_content", "content");

      await projectContextStore.loadFile("ROADMAP", "/home/naomi/myproject");

      expect(get(projectContextStore.isLoading).ROADMAP).toBe(false);
    });

    it("sets isLoading to false even on error", async () => {
      const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
      setMockInvokeResult("read_file_content", new Error("Read error"));

      await projectContextStore.loadFile("STATE", "/home/naomi/myproject");

      expect(get(projectContextStore.isLoading).STATE).toBe(false);
      consoleSpy.mockRestore();
    });

    it("uses correct filename for each file type", async () => {
      const files: ProjectFile[] = ["PROJECT", "REQUIREMENTS", "ROADMAP", "STATE"];
      for (const file of files) {
        setMockInvokeResult("read_file_content", `content for ${file}`);
        await projectContextStore.loadFile(file, "/wd");
        expect(invoke).toHaveBeenCalledWith("read_file_content", {
          path: `/wd/${PROJECT_FILE_NAMES[file]}`,
        });
      }
    });
  });

  describe("saveFile", () => {
    it("calls write_file_content with the correct path and content", async () => {
      setMockInvokeResult("write_file_content", undefined);

      await projectContextStore.saveFile("PROJECT", "# New content", "/home/naomi/myproject");

      expect(invoke).toHaveBeenCalledWith("write_file_content", {
        path: "/home/naomi/myproject/PROJECT.md",
        content: "# New content",
      });
    });

    it("returns true on success", async () => {
      setMockInvokeResult("write_file_content", undefined);

      const result = await projectContextStore.saveFile(
        "PROJECT",
        "# Content",
        "/home/naomi/myproject"
      );

      expect(result).toBe(true);
    });

    it("updates contents store with saved content on success", async () => {
      setMockInvokeResult("write_file_content", undefined);

      const newContent = "# Updated Project\n\nNew content";
      await projectContextStore.saveFile("REQUIREMENTS", newContent, "/home/naomi/myproject");

      expect(get(projectContextStore.contents).REQUIREMENTS).toBe(newContent);
    });

    it("returns false and logs error on failure", async () => {
      const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
      setMockInvokeResult("write_file_content", new Error("Write failed"));

      const result = await projectContextStore.saveFile(
        "ROADMAP",
        "content",
        "/home/naomi/myproject"
      );

      expect(result).toBe(false);
      expect(consoleSpy).toHaveBeenCalledWith(
        "Failed to save project context file:",
        expect.any(Error)
      );
      consoleSpy.mockRestore();
    });

    it("sets isSaving to false after completion", async () => {
      setMockInvokeResult("write_file_content", undefined);

      await projectContextStore.saveFile("STATE", "content", "/home/naomi/myproject");

      expect(get(projectContextStore.isSaving).STATE).toBe(false);
    });

    it("sets isSaving to false even on error", async () => {
      const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
      setMockInvokeResult("write_file_content", new Error("Error"));

      await projectContextStore.saveFile("PROJECT", "content", "/home/naomi/myproject");

      expect(get(projectContextStore.isSaving).PROJECT).toBe(false);
      consoleSpy.mockRestore();
    });
  });

  describe("loadAll", () => {
    it("loads all five files in parallel", async () => {
      setMockInvokeResult("read_file_content", "file content");

      await projectContextStore.loadAll("/home/naomi/myproject");

      const calls = vi.mocked(invoke).mock.calls.filter(([cmd]) => cmd === "read_file_content");
      const paths = calls.map(([, args]) => (args as { path: string }).path);

      expect(paths).toContain("/home/naomi/myproject/PROJECT.md");
      expect(paths).toContain("/home/naomi/myproject/REQUIREMENTS.md");
      expect(paths).toContain("/home/naomi/myproject/ROADMAP.md");
      expect(paths).toContain("/home/naomi/myproject/STATE.md");
      expect(paths).toContain("/home/naomi/myproject/CODEBASE.md");
    });

    it("sets all files isLoading to false after completion", async () => {
      setMockInvokeResult("read_file_content", "content");

      await projectContextStore.loadAll("/home/naomi/myproject");

      const loadingState = get(projectContextStore.isLoading);
      expect(loadingState.PROJECT).toBe(false);
      expect(loadingState.REQUIREMENTS).toBe(false);
      expect(loadingState.ROADMAP).toBe(false);
      expect(loadingState.STATE).toBe(false);
      expect(loadingState.CODEBASE).toBe(false);
    });
  });

  describe("setActiveFile", () => {
    it("updates the activeFile store", () => {
      projectContextStore.setActiveFile("REQUIREMENTS");
      expect(get(projectContextStore.activeFile)).toBe("REQUIREMENTS");

      projectContextStore.setActiveFile("STATE");
      expect(get(projectContextStore.activeFile)).toBe("STATE");

      projectContextStore.setActiveFile("PROJECT");
      expect(get(projectContextStore.activeFile)).toBe("PROJECT");
    });
  });

  describe("getTemplate", () => {
    const files: ProjectFile[] = ["PROJECT", "REQUIREMENTS", "ROADMAP", "STATE"];

    it.each(files)("returns a non-empty string for %s", (file) => {
      const template = projectContextStore.getTemplate(file);
      expect(typeof template).toBe("string");
      expect(template.length).toBeGreaterThan(0);
    });

    it("returns distinct templates for each file type", () => {
      const templates = files.map((f) => projectContextStore.getTemplate(f));
      const uniqueTemplates = new Set(templates);
      expect(uniqueTemplates.size).toBe(files.length);
    });

    it("returns empty string for CODEBASE", () => {
      expect(projectContextStore.getTemplate("CODEBASE")).toBe("");
    });
  });

  describe("mapCodebase", () => {
    const mockScan: ProjectScan = {
      working_dir: "/home/naomi/myproject",
      file_tree: "/home/naomi/myproject/\n├── src/\n└── package.json",
      detected_type: "Node.js",
      key_files: ["package.json"],
    };

    it("calls scan_project with the working directory", async () => {
      setMockInvokeResult("scan_project", mockScan);
      setMockInvokeResult("send_prompt", undefined);

      await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123");

      expect(invoke).toHaveBeenCalledWith("scan_project", {
        workingDir: "/home/naomi/myproject",
      });
    });

    it("calls send_prompt with the conversation id and a non-empty prompt", async () => {
      setMockInvokeResult("scan_project", mockScan);
      setMockInvokeResult("send_prompt", undefined);

      await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123");

      expect(invoke).toHaveBeenCalledWith("send_prompt", {
        conversationId: "conv-123",
        message: expect.stringContaining("CODEBASE.md"),
      });
    });

    it("prompt includes detected project type", async () => {
      setMockInvokeResult("scan_project", mockScan);
      setMockInvokeResult("send_prompt", undefined);

      await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123");

      const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt");
      const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? "";
      expect(message).toContain("Node.js");
    });

    it("prompt includes file tree", async () => {
      setMockInvokeResult("scan_project", mockScan);
      setMockInvokeResult("send_prompt", undefined);

      await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123");

      const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt");
      const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? "";
      expect(message).toContain("package.json");
    });

    it("resets isMappingCodebase to false on error", async () => {
      const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
      setMockInvokeResult("scan_project", new Error("Scan failed"));

      await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123");

      expect(get(projectContextStore.isMappingCodebase)).toBe(false);
      consoleSpy.mockRestore();
    });

    it("logs error when scan_project fails", async () => {
      const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
      setMockInvokeResult("scan_project", new Error("Scan failed"));

      await projectContextStore.mapCodebase("/home/naomi/myproject", "conv-123");

      expect(consoleSpy).toHaveBeenCalledWith("Failed to map codebase:", expect.any(Error));
      consoleSpy.mockRestore();
    });
  });

  describe("finishMapping", () => {
    it("sets isMappingCodebase to false", () => {
      projectContextStore.finishMapping();
      expect(get(projectContextStore.isMappingCodebase)).toBe(false);
    });
  });
});

describe("PROJECT_CONTEXT_SYSTEM_ADDENDUM", () => {
  it("is a non-empty string", () => {
    expect(typeof PROJECT_CONTEXT_SYSTEM_ADDENDUM).toBe("string");
    expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM.length).toBeGreaterThan(0);
  });

  it("mentions all five context file names", () => {
    expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("PROJECT.md");
    expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("REQUIREMENTS.md");
    expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("ROADMAP.md");
    expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("STATE.md");
    expect(PROJECT_CONTEXT_SYSTEM_ADDENDUM).toContain("CODEBASE.md");
  });
});

describe("injectTextStore", () => {
  it("initialises to null", () => {
    expect(get(injectTextStore)).toBeNull();
  });

  it("can be set and read", () => {
    injectTextStore.set("hello world");
    expect(get(injectTextStore)).toBe("hello world");
    injectTextStore.set(null);
  });
});
