import { describe, expect, it } from "bun:test";
import {
  existsSync,
  lstatSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import {
  CODMAP_INIT_MANAGED,
  CODMAP_POINTER_BEGIN,
  CODMAP_POINTER_END,
  ensureGitignoreCodemapPattern,
  listRegularFilesRecursive,
  relPathToAbsSegments,
  resolveBundledAgentMirrorPaths,
  runAgentsInit,
  targetsNeedLinkMode,
  upsertCodemapPointerFile,
} from "./agents-init";
import { CODEMAP_MCP_PERMISSION_ALLOW } from "./agents-init-mcp";
import { resolveAgentsTemplateDir } from "./agents-template-path";
import {
  buildHookBlock,
  isCodemapHookInstalled,
} from "./application/git-hooks";

describe("runAgentsInit", () => {
  it("copies templates into .agents/", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      const ok = await runAgentsInit({ projectRoot: dir, force: true });
      expect(ok).toBe(true);
      const skill = readFileSync(
        join(dir, ".agents", "skills", "codemap", "SKILL.md"),
        "utf-8",
      );
      expect(skill.length).toBeGreaterThan(100);
      expect(
        readFileSync(join(dir, ".agents", "rules", "codemap.md"), "utf-8"),
      ).toContain("codemap");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with --force refreshes only template file paths; user files under rules/ and skills/ remain", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      mkdirSync(join(dir, ".agents", "rules"), { recursive: true });
      mkdirSync(join(dir, ".agents", "skills", "stale"), { recursive: true });
      writeFileSync(join(dir, ".agents", "USER_NOTES.md"), "keep me", "utf-8");
      writeFileSync(
        join(dir, ".agents", "rules", "stale.txt"),
        "user rule",
        "utf-8",
      );
      writeFileSync(
        join(dir, ".agents", "skills", "stale", "SKILL.md"),
        "user skill",
        "utf-8",
      );
      expect(await runAgentsInit({ projectRoot: dir, force: true })).toBe(true);
      expect(readFileSync(join(dir, ".agents", "USER_NOTES.md"), "utf-8")).toBe(
        "keep me",
      );
      expect(
        readFileSync(join(dir, ".agents", "rules", "stale.txt"), "utf-8"),
      ).toBe("user rule");
      expect(
        readFileSync(
          join(dir, ".agents", "skills", "stale", "SKILL.md"),
          "utf-8",
        ),
      ).toBe("user skill");
      expect(
        readFileSync(join(dir, ".agents", "rules", "codemap.md"), "utf-8"),
      ).toContain("codemap");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with mcp and empty targets skips MCP writes", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          mcp: true,
          targets: [],
        }),
      ).toBe(true);
      expect(existsSync(join(dir, ".cursor", "mcp.json"))).toBe(false);
      expect(existsSync(join(dir, ".mcp.json"))).toBe(false);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with amazon-q target writes both Amazon Q MCP files", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["amazon-q"],
          mcp: true,
        }),
      ).toBe(true);
      expect(existsSync(join(dir, ".amazonq", "mcp.json"))).toBe(true);
      expect(existsSync(join(dir, ".amazonq", "default.json"))).toBe(true);
      expect(existsSync(join(dir, ".cursor", "mcp.json"))).toBe(false);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with mcp writes default project-local MCP configs", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      expect(
        await runAgentsInit({ projectRoot: dir, force: true, mcp: true }),
      ).toBe(true);
      expect(existsSync(join(dir, ".cursor", "mcp.json"))).toBe(true);
      const cursor = JSON.parse(
        readFileSync(join(dir, ".cursor", "mcp.json"), "utf-8"),
      ) as { mcpServers: Record<string, { command: string; args: string[] }> };
      expect(cursor.mcpServers.codemap?.command).toBe("npx");
      expect(cursor.mcpServers.codemap?.args[0]).toBe(
        "@stainless-code/codemap@latest",
      );

      expect(existsSync(join(dir, ".mcp.json"))).toBe(true);
      expect(existsSync(join(dir, ".vscode", "mcp.json"))).toBe(true);
      expect(existsSync(join(dir, ".amazonq", "mcp.json"))).toBe(true);
      expect(existsSync(join(dir, ".amazonq", "default.json"))).toBe(true);
      expect(existsSync(join(dir, ".claude", "settings.json"))).toBe(true);
      const settings = JSON.parse(
        readFileSync(join(dir, ".claude", "settings.json"), "utf-8"),
      ) as { permissions: { allow: string[] } };
      expect(settings.permissions.allow).toContain(
        CODEMAP_MCP_PERMISSION_ALLOW,
      );
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit --mcp on existing .agents/ without --force writes MCP only", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      mkdirSync(join(dir, ".agents"), { recursive: true });
      writeFileSync(join(dir, ".agents", "USER.md"), "keep", "utf-8");
      expect(await runAgentsInit({ projectRoot: dir, mcp: true })).toBe(true);
      expect(readFileSync(join(dir, ".agents", "USER.md"), "utf-8")).toBe(
        "keep",
      );
      expect(existsSync(join(dir, ".cursor", "mcp.json"))).toBe(true);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit --git-hooks on existing .agents/ without --force installs hooks only", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      mkdirSync(join(dir, ".agents"), { recursive: true });
      mkdirSync(join(dir, ".git", "hooks"), { recursive: true });
      writeFileSync(join(dir, ".agents", "USER.md"), "keep", "utf-8");
      expect(
        await runAgentsInit({ projectRoot: dir, gitHooks: "install" }),
      ).toBe(true);
      expect(readFileSync(join(dir, ".agents", "USER.md"), "utf-8")).toBe(
        "keep",
      );
      expect(
        isCodemapHookInstalled(join(dir, ".git", "hooks", "post-commit")),
      ).toBe(true);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit --git-hooks --mcp on existing .agents/ installs hooks and MCP", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      mkdirSync(join(dir, ".agents"), { recursive: true });
      mkdirSync(join(dir, ".git", "hooks"), { recursive: true });
      writeFileSync(join(dir, ".agents", "USER.md"), "keep", "utf-8");
      expect(
        await runAgentsInit({
          projectRoot: dir,
          gitHooks: "install",
          mcp: true,
        }),
      ).toBe(true);
      expect(readFileSync(join(dir, ".agents", "USER.md"), "utf-8")).toBe(
        "keep",
      );
      expect(
        isCodemapHookInstalled(join(dir, ".git", "hooks", "post-commit")),
      ).toBe(true);
      expect(existsSync(join(dir, ".cursor", "mcp.json"))).toBe(true);
      expect(existsSync(join(dir, ".mcp.json"))).toBe(true);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit --git-hooks --targets cursor on existing .agents/ composes hooks and wiring", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      mkdirSync(join(dir, ".agents", "rules"), { recursive: true });
      mkdirSync(join(dir, ".agents", "skills", "codemap"), {
        recursive: true,
      });
      mkdirSync(join(dir, ".git", "hooks"), { recursive: true });
      writeFileSync(
        join(dir, ".agents", "rules", "codemap.md"),
        `${CODMAP_INIT_MANAGED}\n`,
        "utf-8",
      );
      writeFileSync(
        join(dir, ".agents", "skills", "codemap", "SKILL.md"),
        `${CODMAP_INIT_MANAGED}\n`,
        "utf-8",
      );
      expect(
        await runAgentsInit({
          projectRoot: dir,
          gitHooks: "install",
          targets: ["cursor"],
        }),
      ).toBe(true);
      expect(
        isCodemapHookInstalled(join(dir, ".git", "hooks", "post-commit")),
      ).toBe(true);
      expect(existsSync(join(dir, ".cursor", "rules", "codemap.mdc"))).toBe(
        true,
      );
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit --mcp --targets cursor on existing .agents/ composes MCP and wiring", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      mkdirSync(join(dir, ".agents", "rules"), { recursive: true });
      mkdirSync(join(dir, ".agents", "skills", "codemap"), {
        recursive: true,
      });
      writeFileSync(
        join(dir, ".agents", "rules", "codemap.md"),
        `${CODMAP_INIT_MANAGED}\n`,
        "utf-8",
      );
      writeFileSync(
        join(dir, ".agents", "skills", "codemap", "SKILL.md"),
        `${CODMAP_INIT_MANAGED}\n`,
        "utf-8",
      );
      expect(
        await runAgentsInit({
          projectRoot: dir,
          mcp: true,
          targets: ["cursor"],
        }),
      ).toBe(true);
      expect(existsSync(join(dir, ".cursor", "mcp.json"))).toBe(true);
      expect(existsSync(join(dir, ".cursor", "rules", "codemap.mdc"))).toBe(
        true,
      );
      expect(existsSync(join(dir, ".mcp.json"))).toBe(false);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit --no-git-hooks --mcp uninstalls hooks and writes MCP", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      mkdirSync(join(dir, ".git", "hooks"), { recursive: true });
      writeFileSync(
        join(dir, ".git", "hooks", "post-commit"),
        `echo keep\n${buildHookBlock()}`,
        "utf-8",
      );
      expect(
        await runAgentsInit({
          projectRoot: dir,
          gitHooks: "uninstall",
          mcp: true,
        }),
      ).toBe(true);
      const hook = readFileSync(
        join(dir, ".git", "hooks", "post-commit"),
        "utf-8",
      );
      expect(hook).toContain("echo keep");
      expect(hook).not.toContain("CODEMAP_HOOK_BEGIN");
      expect(existsSync(join(dir, ".cursor", "mcp.json"))).toBe(true);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("listRegularFilesRecursive matches bundled rules and skills files", async () => {
    const root = resolveAgentsTemplateDir();
    const rules = listRegularFilesRecursive(join(root, "rules")).sort();
    const skills = listRegularFilesRecursive(join(root, "skills")).sort();
    expect(rules).toContain("codemap.md");
    expect(skills).toContain("codemap/SKILL.md");
  });

  it("returns false when .agents exists without force", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      mkdirSync(join(dir, ".agents"), { recursive: true });
      expect(await runAgentsInit({ projectRoot: dir, force: false })).toBe(
        false,
      );
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("resolveAgentsTemplateDir points at templates/agents", async () => {
    const p = resolveAgentsTemplateDir().replace(/\\/g, "/");
    expect(p.endsWith("/templates/agents")).toBe(true);
  });

  it("ensureGitignoreCodemapPattern writes <state-dir>/.gitignore (root untouched)", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      const rootGi = join(dir, ".gitignore");
      writeFileSync(rootGi, "node_modules/\n", "utf-8");
      ensureGitignoreCodemapPattern(dir);
      expect(readFileSync(rootGi, "utf-8")).toBe("node_modules/\n");
      const stateGi = join(dir, ".codemap", ".gitignore");
      expect(existsSync(stateGi)).toBe(true);
      expect(readFileSync(stateGi, "utf-8")).toContain("index.db");
      expect(readFileSync(stateGi, "utf-8")).toContain("audit-cache/");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("ensureGitignoreCodemapPattern is idempotent (no rewrite on steady state)", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      ensureGitignoreCodemapPattern(dir);
      const stateGi = join(dir, ".codemap", ".gitignore");
      const before = readFileSync(stateGi, "utf-8");
      ensureGitignoreCodemapPattern(dir);
      expect(readFileSync(stateGi, "utf-8")).toBe(before);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit calls the reconciler — produces <state-dir>/.gitignore", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      expect(await runAgentsInit({ projectRoot: dir, force: true })).toBe(true);
      const stateGi = join(dir, ".codemap", ".gitignore");
      expect(existsSync(stateGi)).toBe(true);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with Cursor target copies into .cursor/", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["cursor"],
          linkMode: "copy",
        }),
      ).toBe(true);
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).toContain("codemap");
      expect(
        readFileSync(
          join(dir, ".cursor", "skills", "codemap", "SKILL.md"),
          "utf-8",
        ).length,
      ).toBeGreaterThan(100);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with Cursor symlink creates per-file symlinks, not directory symlinks", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["cursor"],
          linkMode: "symlink",
        }),
      ).toBe(true);
      const rulesDir = join(dir, ".cursor", "rules");
      const skillsDir = join(dir, ".cursor", "skills");
      expect(lstatSync(rulesDir).isSymbolicLink()).toBe(false);
      expect(lstatSync(skillsDir).isSymbolicLink()).toBe(false);
      expect(lstatSync(rulesDir).isDirectory()).toBe(true);
      expect(lstatSync(skillsDir).isDirectory()).toBe(true);
      for (const rel of resolveBundledAgentMirrorPaths().ruleFiles) {
        const cursorRel = rel.endsWith(".md") ? rel.slice(0, -3) + ".mdc" : rel;
        expect(
          lstatSync(
            join(dir, ".cursor", "rules", ...cursorRel.split("/")),
          ).isSymbolicLink(),
        ).toBe(true);
      }
      for (const rel of resolveBundledAgentMirrorPaths().skillFiles) {
        expect(
          lstatSync(
            join(dir, ".cursor", "skills", ...rel.split("/")),
          ).isSymbolicLink(),
        ).toBe(true);
      }
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with claude-md writes CLAUDE.md", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["claude-md"],
        }),
      ).toBe(true);
      const md = readFileSync(join(dir, "CLAUDE.md"), "utf-8");
      expect(md).toContain("Codemap");
      expect(md).toContain(CODMAP_POINTER_BEGIN);
      expect(md).toContain(CODMAP_POINTER_END);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("targetsNeedLinkMode is true only for symlink-style integrations", async () => {
    expect(targetsNeedLinkMode([])).toBe(false);
    expect(targetsNeedLinkMode(["claude-md", "copilot"])).toBe(false);
    expect(targetsNeedLinkMode(["cursor"])).toBe(true);
    expect(targetsNeedLinkMode(["windsurf", "agents-md"])).toBe(true);
  });

  it("runAgentsInit writes Copilot and pointer files", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["copilot", "agents-md", "gemini-md"],
        }),
      ).toBe(true);
      expect(
        readFileSync(join(dir, ".github", "copilot-instructions.md"), "utf-8"),
      ).toContain("Copilot");
      expect(readFileSync(join(dir, "AGENTS.md"), "utf-8")).toContain(
        CODMAP_POINTER_BEGIN,
      );
      expect(readFileSync(join(dir, "GEMINI.md"), "utf-8")).toContain(
        CODMAP_POINTER_BEGIN,
      );
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with Windsurf copies .windsurf/rules", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["windsurf"],
          linkMode: "copy",
        }),
      ).toBe(true);
      expect(
        readFileSync(join(dir, ".windsurf", "rules", "codemap.md"), "utf-8"),
      ).toContain("codemap");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit does not mirror custom .agents rules to .cursor", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      await runAgentsInit({ projectRoot: dir, force: true });
      writeFileSync(
        join(dir, ".agents", "rules", "user-custom.md"),
        "# User agents rule\n",
        "utf-8",
      );
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: false,
          targets: ["cursor"],
          linkMode: "copy",
        }),
      ).toBe(true);
      expect(existsSync(join(dir, ".cursor", "rules", "user-custom.mdc"))).toBe(
        false,
      );
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).toContain(CODMAP_INIT_MANAGED);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit wires Cursor alongside existing user rules", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      await runAgentsInit({ projectRoot: dir, force: true });
      mkdirSync(join(dir, ".cursor", "rules"), { recursive: true });
      writeFileSync(
        join(dir, ".cursor", "rules", "user-custom.mdc"),
        "# User rule\n",
        "utf-8",
      );
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: false,
          targets: ["cursor"],
          linkMode: "copy",
        }),
      ).toBe(true);
      expect(
        readFileSync(join(dir, ".cursor", "rules", "user-custom.mdc"), "utf-8"),
      ).toBe("# User rule\n");
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).toContain("codemap");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with --force replaces only codemap-managed mirror files under .cursor/rules", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      await runAgentsInit({ projectRoot: dir, force: true });
      mkdirSync(join(dir, ".cursor", "rules"), { recursive: true });
      writeFileSync(
        join(dir, ".cursor", "rules", "user-custom.mdc"),
        "# User rule\n",
        "utf-8",
      );
      writeFileSync(
        join(dir, ".cursor", "rules", "codemap.mdc"),
        `${CODMAP_INIT_MANAGED}\nstale codemap mirror`,
        "utf-8",
      );
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["cursor"],
          linkMode: "copy",
        }),
      ).toBe(true);
      expect(
        readFileSync(join(dir, ".cursor", "rules", "user-custom.mdc"), "utf-8"),
      ).toBe("# User rule\n");
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).toContain("codemap");
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).not.toContain("stale codemap mirror");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with --force refuses to overwrite mirror files without codemap-init marker", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      await runAgentsInit({ projectRoot: dir, force: true });
      mkdirSync(join(dir, ".cursor", "rules"), { recursive: true });
      writeFileSync(
        join(dir, ".cursor", "rules", "codemap.mdc"),
        "# My team codemap override\n",
        "utf-8",
      );
      await expect(
        runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["cursor"],
          linkMode: "copy",
        }),
      ).rejects.toThrow(/not codemap-managed/);
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).toBe("# My team codemap override\n");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with --force migrates legacy copy-mode mirrors without codemap-init marker", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      await runAgentsInit({ projectRoot: dir, force: true });
      mkdirSync(join(dir, ".cursor", "rules"), { recursive: true });
      writeFileSync(
        join(dir, ".cursor", "rules", "codemap.mdc"),
        `${readFileSync(join(dir, ".agents", "rules", "codemap.md"), "utf-8")
          .replace(CODMAP_INIT_MANAGED, "")
          .trim()}\nstale mirror line`,
        "utf-8",
      );
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["cursor"],
          linkMode: "copy",
        }),
      ).toBe(true);
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).toContain(CODMAP_INIT_MANAGED);
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).not.toContain("stale mirror line");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with --force migrates legacy mirror via symlink refresh", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      await runAgentsInit({
        projectRoot: dir,
        force: true,
        targets: ["cursor"],
        linkMode: "copy",
      });
      writeFileSync(
        join(dir, ".cursor", "rules", "codemap.mdc"),
        `${readFileSync(join(dir, ".agents", "rules", "codemap.md"), "utf-8")
          .replace(CODMAP_INIT_MANAGED, "")
          .trim()}\nstale mirror line`,
        "utf-8",
      );
      expect(
        await runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["cursor"],
          linkMode: "symlink",
        }),
      ).toBe(true);
      expect(
        lstatSync(
          join(dir, ".cursor", "rules", "codemap.mdc"),
        ).isSymbolicLink(),
      ).toBe(true);
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).toContain(CODMAP_INIT_MANAGED);
      expect(
        readFileSync(join(dir, ".cursor", "rules", "codemap.mdc"), "utf-8"),
      ).not.toContain("stale mirror line");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("runAgentsInit with --force refuses symlink refresh when dest is a non-managed file", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-agents-"));
    try {
      await runAgentsInit({
        projectRoot: dir,
        force: true,
        targets: ["cursor"],
        linkMode: "symlink",
      });
      rmSync(join(dir, ".cursor", "rules", "codemap.mdc"));
      writeFileSync(
        join(dir, ".cursor", "rules", "codemap.mdc"),
        "# My team codemap override\n",
        "utf-8",
      );
      await expect(
        runAgentsInit({
          projectRoot: dir,
          force: true,
          targets: ["cursor"],
          linkMode: "symlink",
        }),
      ).rejects.toThrow(/not codemap-managed/);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });
});

/** Minimal inner block for pointer tests (matches legacy migration heuristic). */
const POINTER_INNER_TEST = `# Codemap

This project uses [Codemap](https://github.com/stainless-code/codemap) — test.

- **Skill:** \`.agents/skills/codemap/SKILL.md\`
- **CLI:** \`codemap query "SELECT 1"\` for SQL
- **Rules:** \`.agents/rules/\`
`;

function wrapPointerTest(inner: string): string {
  return `${CODMAP_POINTER_BEGIN}\n${inner.trim()}\n${CODMAP_POINTER_END}\n`;
}

describe("upsertCodemapPointerFile", () => {
  it("appends managed section to existing non-Codemap file", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-"));
    const p = join(dir, "AGENTS.md");
    try {
      writeFileSync(p, "# Team\n\nOur project.\n", "utf-8");
      upsertCodemapPointerFile(p, POINTER_INNER_TEST, "AGENTS.md", false);
      const out = readFileSync(p, "utf-8");
      expect(out).toContain("# Team");
      expect(out).toContain("Our project.");
      expect(out).toContain(CODMAP_POINTER_BEGIN);
      expect(out.indexOf("# Team")).toBeLessThan(
        out.indexOf(CODMAP_POINTER_BEGIN),
      );
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("replaces managed section in place on second run (no duplicate blocks)", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-"));
    const p = join(dir, "NOTE.md");
    try {
      writeFileSync(p, wrapPointerTest("FIRST"), "utf-8");
      upsertCodemapPointerFile(
        p,
        "SECOND\n\nstill https://github.com/stainless-code/codemap\n`.agents/skills/codemap`\n`codemap query`",
        "NOTE.md",
        false,
      );
      const out = readFileSync(p, "utf-8");
      expect(out).toContain("SECOND");
      expect(out).not.toContain("FIRST");
      expect(out.match(new RegExp(CODMAP_POINTER_BEGIN, "g"))?.length).toBe(1);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("migrates legacy unmarked Codemap pointer file to managed section", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-"));
    const p = join(dir, "CLAUDE.md");
    try {
      writeFileSync(p, POINTER_INNER_TEST, "utf-8");
      upsertCodemapPointerFile(
        p,
        `${POINTER_INNER_TEST}\n\n## Extra\n\nMigrated.\n`,
        "CLAUDE.md",
        false,
      );
      const out = readFileSync(p, "utf-8");
      expect(out).toContain(CODMAP_POINTER_BEGIN);
      expect(out).toContain("Migrated.");
      expect(out).not.toContain(`${POINTER_INNER_TEST}\n${POINTER_INNER_TEST}`);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("replaces ALL managed sections when file has two blocks", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-"));
    const p = join(dir, "NOTE.md");
    try {
      writeFileSync(
        p,
        `${wrapPointerTest("FIRST")}\nOther content\n${wrapPointerTest("SECOND")}`,
        "utf-8",
      );
      upsertCodemapPointerFile(
        p,
        "UPDATED\n\nstill https://github.com/stainless-code/codemap\n`.agents/skills/codemap`\n`codemap query`",
        "NOTE.md",
        false,
      );
      const out = readFileSync(p, "utf-8");
      expect(out).toContain("UPDATED");
      expect(out).not.toContain("FIRST");
      expect(out).not.toContain("SECOND");
      expect(out.match(new RegExp(CODMAP_POINTER_BEGIN, "g"))?.length).toBe(1);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  it("--force refreshes pointer section without dropping user content", async () => {
    const dir = mkdtempSync(join(tmpdir(), "codemap-pointer-"));
    const p = join(dir, "AGENTS.md");
    try {
      writeFileSync(p, "# Keep me\n\nLots of custom content.\n", "utf-8");
      upsertCodemapPointerFile(p, POINTER_INNER_TEST, "AGENTS.md", true);
      const out = readFileSync(p, "utf-8");
      expect(out).toContain("# Keep me");
      expect(out).toContain("Lots of custom content.");
      expect(out).toContain(CODMAP_POINTER_BEGIN);
      expect(out).toContain("stainless-code/codemap");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });
});

describe("relPathToAbsSegments — defence-in-depth path safety", () => {
  it("returns segments for a normal relative path", async () => {
    expect(relPathToAbsSegments("rules/codemap.md")).toEqual([
      "rules",
      "codemap.md",
    ]);
  });

  it("filters empty segments (leading / trailing / double slashes)", async () => {
    expect(relPathToAbsSegments("/rules//codemap.md/")).toEqual([
      "rules",
      "codemap.md",
    ]);
  });

  it("rejects `..` segment", async () => {
    expect(() => relPathToAbsSegments("../etc/passwd")).toThrow(
      /refusing path with ".." segment/,
    );
  });

  it("rejects `..` segment in the middle of the path", async () => {
    expect(() => relPathToAbsSegments("rules/../../etc/passwd")).toThrow(
      /refusing path with ".." segment/,
    );
  });

  it("rejects `.` segment", async () => {
    expect(() => relPathToAbsSegments("rules/./codemap.md")).toThrow(
      /refusing path with "." segment/,
    );
  });
});
