import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.js";
import { ExtensionRunner } from "../src/core/extensions/runner.js";
import { ModelRegistry } from "../src/core/model-registry.js";
import { DefaultResourceLoader } from "../src/core/resource-loader.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import type { Skill } from "../src/core/skills.js";

describe("DefaultResourceLoader", () => {
  let tempDir: string;
  let agentDir: string;
  let cwd: string;

  beforeEach(() => {
    tempDir = join(
      tmpdir(),
      `rl-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
    );
    agentDir = join(tempDir, "agent");
    cwd = join(tempDir, "project");
    mkdirSync(agentDir, { recursive: true });
    mkdirSync(cwd, { recursive: true });
  });

  afterEach(() => {
    rmSync(tempDir, { recursive: true, force: true });
  });

  describe("reload", () => {
    it("should initialize with empty results before reload", () => {
      const loader = new DefaultResourceLoader({ cwd, agentDir });

      expect(loader.getExtensions().extensions).toEqual([]);
      expect(loader.getSkills().skills).toEqual([]);
      expect(loader.getPrompts().prompts).toEqual([]);
      expect(loader.getThemes().themes).toEqual([]);
    });

    it("should discover skills from agentDir", async () => {
      const skillsDir = join(agentDir, "skills");
      mkdirSync(skillsDir, { recursive: true });
      writeFileSync(
        join(skillsDir, "test-skill.md"),
        `---
name: test-skill
description: A test skill
---
Skill content here.`,
      );

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      const { skills } = loader.getSkills();
      expect(skills.some((s) => s.name === "test-skill")).toBe(true);
    });

    it("should ignore extra markdown files in auto-discovered skill dirs", async () => {
      const skillDir = join(agentDir, "skills", "pi-skills", "browser-tools");
      mkdirSync(skillDir, { recursive: true });
      writeFileSync(
        join(skillDir, "SKILL.md"),
        `---
name: browser-tools
description: Browser tools
---
Skill content here.`,
      );
      writeFileSync(join(skillDir, "EFFICIENCY.md"), "No frontmatter here");

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      const { skills, diagnostics } = loader.getSkills();
      expect(skills.some((s) => s.name === "browser-tools")).toBe(true);
      expect(diagnostics.some((d) => d.path?.endsWith("EFFICIENCY.md"))).toBe(
        false,
      );
    });

    it("should discover prompts from agentDir", async () => {
      const promptsDir = join(agentDir, "prompts");
      mkdirSync(promptsDir, { recursive: true });
      writeFileSync(
        join(promptsDir, "test-prompt.md"),
        `---
description: A test prompt
---
Prompt content.`,
      );

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      const { prompts } = loader.getPrompts();
      expect(prompts.some((p) => p.name === "test-prompt")).toBe(true);
    });

    it("should prefer project resources over user on name collisions", async () => {
      const userPromptsDir = join(agentDir, "prompts");
      const projectPromptsDir = join(cwd, ".pi", "prompts");
      mkdirSync(userPromptsDir, { recursive: true });
      mkdirSync(projectPromptsDir, { recursive: true });
      const userPromptPath = join(userPromptsDir, "commit.md");
      const projectPromptPath = join(projectPromptsDir, "commit.md");
      writeFileSync(userPromptPath, "User prompt");
      writeFileSync(projectPromptPath, "Project prompt");

      const userSkillDir = join(agentDir, "skills", "collision-skill");
      const projectSkillDir = join(cwd, ".pi", "skills", "collision-skill");
      mkdirSync(userSkillDir, { recursive: true });
      mkdirSync(projectSkillDir, { recursive: true });
      const userSkillPath = join(userSkillDir, "SKILL.md");
      const projectSkillPath = join(projectSkillDir, "SKILL.md");
      writeFileSync(
        userSkillPath,
        `---
name: collision-skill
description: user
---
User skill`,
      );
      writeFileSync(
        projectSkillPath,
        `---
name: collision-skill
description: project
---
Project skill`,
      );

      const baseTheme = JSON.parse(
        readFileSync(
          join(
            process.cwd(),
            "src",
            "modes",
            "interactive",
            "theme",
            "dark.json",
          ),
          "utf-8",
        ),
      ) as { name: string; vars?: Record<string, string> };
      baseTheme.name = "collision-theme";
      const userThemePath = join(agentDir, "themes", "collision.json");
      const projectThemePath = join(cwd, ".pi", "themes", "collision.json");
      mkdirSync(join(agentDir, "themes"), { recursive: true });
      mkdirSync(join(cwd, ".pi", "themes"), { recursive: true });
      writeFileSync(userThemePath, JSON.stringify(baseTheme, null, 2));
      if (baseTheme.vars) {
        baseTheme.vars.accent = "#ff00ff";
      }
      writeFileSync(projectThemePath, JSON.stringify(baseTheme, null, 2));

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      const prompt = loader
        .getPrompts()
        .prompts.find((p) => p.name === "commit");
      expect(prompt?.filePath).toBe(projectPromptPath);

      const skill = loader
        .getSkills()
        .skills.find((s) => s.name === "collision-skill");
      expect(skill?.filePath).toBe(projectSkillPath);

      const theme = loader
        .getThemes()
        .themes.find((t) => t.name === "collision-theme");
      expect(theme?.sourcePath).toBe(projectThemePath);
    });

    it("should keep both extensions loaded when command names collide", async () => {
      const userExtDir = join(agentDir, "extensions");
      const projectExtDir = join(cwd, ".pi", "extensions");
      mkdirSync(userExtDir, { recursive: true });
      mkdirSync(projectExtDir, { recursive: true });

      writeFileSync(
        join(projectExtDir, "project.ts"),
        `export default function(pi) {
	pi.registerCommand("deploy", {
		description: "project deploy",
		handler: async () => {},
	});
	pi.registerCommand("project-only", {
		description: "project only",
		handler: async () => {},
	});
}`,
      );

      writeFileSync(
        join(userExtDir, "user.ts"),
        `export default function(pi) {
	pi.registerCommand("deploy", {
		description: "user deploy",
		handler: async () => {},
	});
	pi.registerCommand("user-only", {
		description: "user only",
		handler: async () => {},
	});
}`,
      );

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      const extensionsResult = loader.getExtensions();
      expect(extensionsResult.extensions).toHaveLength(2);
      expect(
        extensionsResult.errors.some((e) =>
          e.error.includes('Command "/deploy" conflicts'),
        ),
      ).toBe(true);

      const sessionManager = SessionManager.inMemory();
      const authStorage = AuthStorage.create(join(tempDir, "auth.json"));
      const modelRegistry = new ModelRegistry(authStorage);
      const runner = new ExtensionRunner(
        extensionsResult.extensions,
        extensionsResult.runtime,
        cwd,
        sessionManager,
        modelRegistry,
      );

      expect(runner.getCommand("deploy")?.description).toBe("project deploy");
      expect(runner.getCommand("project-only")?.description).toBe(
        "project only",
      );
      expect(runner.getCommand("user-only")?.description).toBe("user only");

      const commandNames = runner.getRegisteredCommands().map((c) => c.name);
      expect(commandNames.filter((name) => name === "deploy")).toHaveLength(1);
    });

    it("should honor overrides for auto-discovered resources", async () => {
      const settingsManager = SettingsManager.inMemory();
      settingsManager.setExtensionPaths(["-extensions/disabled.ts"]);
      settingsManager.setSkillPaths(["-skills/skip-skill"]);
      settingsManager.setPromptTemplatePaths(["-prompts/skip.md"]);
      settingsManager.setThemePaths(["-themes/skip.json"]);

      const extensionsDir = join(agentDir, "extensions");
      mkdirSync(extensionsDir, { recursive: true });
      writeFileSync(
        join(extensionsDir, "disabled.ts"),
        "export default function() {}",
      );

      const skillDir = join(agentDir, "skills", "skip-skill");
      mkdirSync(skillDir, { recursive: true });
      writeFileSync(
        join(skillDir, "SKILL.md"),
        `---
name: skip-skill
description: Skip me
---
Content`,
      );

      const promptsDir = join(agentDir, "prompts");
      mkdirSync(promptsDir, { recursive: true });
      writeFileSync(join(promptsDir, "skip.md"), "Skip prompt");

      const themesDir = join(agentDir, "themes");
      mkdirSync(themesDir, { recursive: true });
      writeFileSync(join(themesDir, "skip.json"), "{}");

      const loader = new DefaultResourceLoader({
        cwd,
        agentDir,
        settingsManager,
      });
      await loader.reload();

      const { extensions } = loader.getExtensions();
      const { skills } = loader.getSkills();
      const { prompts } = loader.getPrompts();
      const { themes } = loader.getThemes();

      expect(extensions.some((e) => e.path.endsWith("disabled.ts"))).toBe(
        false,
      );
      expect(skills.some((s) => s.name === "skip-skill")).toBe(false);
      expect(prompts.some((p) => p.name === "skip")).toBe(false);
      expect(themes.some((t) => t.sourcePath?.endsWith("skip.json"))).toBe(
        false,
      );
    });

    it("should discover AGENTS.md context files", async () => {
      writeFileSync(
        join(cwd, "AGENTS.md"),
        "# Project Guidelines\n\nBe helpful.",
      );

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      const { agentsFiles } = loader.getAgentsFiles();
      expect(agentsFiles.some((f) => f.path.includes("AGENTS.md"))).toBe(true);
    });

    it("should discover SOUL.md from the project root", async () => {
      writeFileSync(join(cwd, "SOUL.md"), "# Soul\n\nBe less corporate.");

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      const { agentsFiles } = loader.getAgentsFiles();
      expect(agentsFiles.some((f) => f.path.endsWith("SOUL.md"))).toBe(true);
    });

    it("should discover companion context files from the default workspace", async () => {
      const workspaceDir = join(tempDir, "workspace");
      const appDir = join(tempDir, "apps", "todo-app");
      mkdirSync(workspaceDir, { recursive: true });
      mkdirSync(appDir, { recursive: true });
      writeFileSync(join(workspaceDir, "IDENTITY.md"), "# Identity\n\nPi");
      writeFileSync(join(workspaceDir, "TOOLS.md"), "# Tools\n\nUse ~/.pi");

      const loader = new DefaultResourceLoader({ cwd: appDir, agentDir });
      await loader.reload();

      const { agentsFiles } = loader.getAgentsFiles();
      expect(agentsFiles.some((f) => f.path.endsWith("IDENTITY.md"))).toBe(
        true,
      );
      expect(agentsFiles.some((f) => f.path.endsWith("TOOLS.md"))).toBe(true);
    });

    it("should discover SYSTEM.md from cwd/.pi", async () => {
      const piDir = join(cwd, ".pi");
      mkdirSync(piDir, { recursive: true });
      writeFileSync(join(piDir, "SYSTEM.md"), "You are a helpful assistant.");

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      expect(loader.getSystemPrompt()).toBe("You are a helpful assistant.");
    });

    it("should discover APPEND_SYSTEM.md", async () => {
      const piDir = join(cwd, ".pi");
      mkdirSync(piDir, { recursive: true });
      writeFileSync(
        join(piDir, "APPEND_SYSTEM.md"),
        "Additional instructions.",
      );

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      expect(loader.getAppendSystemPrompt()).toContain(
        "Additional instructions.",
      );
    });
  });

  describe("extendResources", () => {
    it("should load skills and prompts with extension metadata", async () => {
      const extraSkillDir = join(tempDir, "extra-skills", "extra-skill");
      mkdirSync(extraSkillDir, { recursive: true });
      const skillPath = join(extraSkillDir, "SKILL.md");
      writeFileSync(
        skillPath,
        `---
name: extra-skill
description: Extra skill
---
Extra content`,
      );

      const extraPromptDir = join(tempDir, "extra-prompts");
      mkdirSync(extraPromptDir, { recursive: true });
      const promptPath = join(extraPromptDir, "extra.md");
      writeFileSync(
        promptPath,
        `---
description: Extra prompt
---
Extra prompt content`,
      );

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      loader.extendResources({
        skillPaths: [
          {
            path: extraSkillDir,
            metadata: {
              source: "extension:extra",
              scope: "temporary",
              origin: "top-level",
              baseDir: extraSkillDir,
            },
          },
        ],
        promptPaths: [
          {
            path: promptPath,
            metadata: {
              source: "extension:extra",
              scope: "temporary",
              origin: "top-level",
              baseDir: extraPromptDir,
            },
          },
        ],
      });

      const { skills } = loader.getSkills();
      expect(skills.some((skill) => skill.name === "extra-skill")).toBe(true);

      const { prompts } = loader.getPrompts();
      expect(prompts.some((prompt) => prompt.name === "extra")).toBe(true);

      const metadata = loader.getPathMetadata();
      expect(metadata.get(skillPath)?.source).toBe("extension:extra");
      expect(metadata.get(promptPath)?.source).toBe("extension:extra");
    });
  });

  describe("noSkills option", () => {
    it("should skip skill discovery when noSkills is true", async () => {
      const skillsDir = join(agentDir, "skills");
      mkdirSync(skillsDir, { recursive: true });
      writeFileSync(
        join(skillsDir, "test-skill.md"),
        `---
name: test-skill
description: A test skill
---
Content`,
      );

      const loader = new DefaultResourceLoader({
        cwd,
        agentDir,
        noSkills: true,
      });
      await loader.reload();

      const { skills } = loader.getSkills();
      expect(skills).toEqual([]);
    });

    it("should still load additional skill paths when noSkills is true", async () => {
      const customSkillDir = join(tempDir, "custom-skills");
      mkdirSync(customSkillDir, { recursive: true });
      writeFileSync(
        join(customSkillDir, "custom.md"),
        `---
name: custom
description: Custom skill
---
Content`,
      );

      const loader = new DefaultResourceLoader({
        cwd,
        agentDir,
        noSkills: true,
        additionalSkillPaths: [customSkillDir],
      });
      await loader.reload();

      const { skills } = loader.getSkills();
      expect(skills.some((s) => s.name === "custom")).toBe(true);
    });
  });

  describe("override functions", () => {
    it("should apply skillsOverride", async () => {
      const injectedSkill: Skill = {
        name: "injected",
        description: "Injected skill",
        filePath: "/fake/path",
        baseDir: "/fake",
        source: "custom",
        disableModelInvocation: false,
      };
      const loader = new DefaultResourceLoader({
        cwd,
        agentDir,
        skillsOverride: () => ({
          skills: [injectedSkill],
          diagnostics: [],
        }),
      });
      await loader.reload();

      const { skills } = loader.getSkills();
      expect(skills).toHaveLength(1);
      expect(skills[0].name).toBe("injected");
    });

    it("should apply systemPromptOverride", async () => {
      const loader = new DefaultResourceLoader({
        cwd,
        agentDir,
        systemPromptOverride: () => "Custom system prompt",
      });
      await loader.reload();

      expect(loader.getSystemPrompt()).toBe("Custom system prompt");
    });
  });

  describe("extension conflict detection", () => {
    it("should detect tool conflicts between extensions", async () => {
      // Create two extensions that register the same tool
      const ext1Dir = join(agentDir, "extensions", "ext1");
      const ext2Dir = join(agentDir, "extensions", "ext2");
      mkdirSync(ext1Dir, { recursive: true });
      mkdirSync(ext2Dir, { recursive: true });

      writeFileSync(
        join(ext1Dir, "index.ts"),
        `
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
export default function(pi: ExtensionAPI) {
  pi.registerTool({
    name: "duplicate-tool",
    description: "First",
    parameters: Type.Object({}),
    execute: async () => ({ result: "1" }),
  });
}`,
      );

      writeFileSync(
        join(ext2Dir, "index.ts"),
        `
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
export default function(pi: ExtensionAPI) {
  pi.registerTool({
    name: "duplicate-tool",
    description: "Second",
    parameters: Type.Object({}),
    execute: async () => ({ result: "2" }),
  });
}`,
      );

      const loader = new DefaultResourceLoader({ cwd, agentDir });
      await loader.reload();

      const { errors } = loader.getExtensions();
      expect(
        errors.some(
          (e) =>
            e.error.includes("duplicate-tool") && e.error.includes("conflicts"),
        ),
      ).toBe(true);
    });
  });
});
