import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
  importSkillDirectory,
  importSkillFromFiles,
  prepareRunSkillPlugin,
  readSkillFiles,
} from "../../src/agent/skillStore.js";

describe("content-addressed Agent skill store", () => {
  const roots: string[] = [];

  afterEach(async () => {
    await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
  });

  it("imports a skill into an immutable digest directory and materializes a selected run plugin", async () => {
    const root = await makeRoot();
    const source = await makeSkill(root, "typst", "Typst help");
    const storeRoot = join(root, "store");

    const installed = await importSkillDirectory({ sourceDir: source, storeRoot });
    expect(installed).toMatchObject({ name: "typst", description: "Typst help" });
    expect(installed.contentDigest).toMatch(/^[a-f0-9]{64}$/);
    await expect(readFile(join(storeRoot, "versions", installed.contentDigest, "SKILL.md"), "utf8"))
      .resolves.toContain("name: typst");

    const plugin = await prepareRunSkillPlugin({
      storeRoot,
      runId: "run-1",
      skills: [{ name: "typst", version: "0.15.0", contentDigest: installed.contentDigest }],
    });
    expect(plugin).not.toBeNull();
    expect(plugin?.skillIds).toEqual(["cph-runtime:typst"]);
    await expect(readFile(join(plugin!.root, "skills", "typst", "reference.md"), "utf8"))
      .resolves.toBe("reference\n");

    await plugin?.cleanup();
    await expect(readFile(join(plugin!.root, ".claude-plugin", "plugin.json"), "utf8"))
      .rejects.toMatchObject({ code: "ENOENT" });
  });

  it("rejects symlinks and detects content tampering before a run", async () => {
    const root = await makeRoot();
    const source = await makeSkill(root, "outline", "Outline");
    await symlink(join(source, "reference.md"), join(source, "link.md"));
    await expect(importSkillDirectory({ sourceDir: source, storeRoot: join(root, "store") }))
      .rejects.toThrow(/symlink/);
    await rm(join(source, "link.md"));

    const storeRoot = join(root, "store");
    const installed = await importSkillDirectory({ sourceDir: source, storeRoot });
    await writeFile(join(storeRoot, "versions", installed.contentDigest, "reference.md"), "tampered\n");
    await expect(prepareRunSkillPlugin({
      storeRoot,
      runId: "run-2",
      skills: [{ name: "outline", version: "1", contentDigest: installed.contentDigest }],
    })).rejects.toThrow(/content digest mismatch/);
  });

  it("imports a skill from an in-memory file list and reads it back", async () => {
    const root = await makeRoot();
    const storeRoot = join(root, "store");
    const manifest = `---\nname: web-skill\ndescription: Web uploaded\n---\n# web-skill\n`;
    const files = [
      { path: "SKILL.md", bytes: Buffer.from(manifest, "utf8") },
      { path: "reference.md", bytes: Buffer.from("ref\n", "utf8") },
    ];
    const installed = await importSkillFromFiles({ files, storeRoot });
    expect(installed).toMatchObject({ name: "web-skill", description: "Web uploaded" });
    expect(installed.contentDigest).toMatch(/^[a-f0-9]{64}$/);

    const readBack = await readSkillFiles({ storeRoot, contentDigest: installed.contentDigest });
    expect(readBack).toHaveLength(2);
    const byPath = Object.fromEntries(readBack.map((f) => [f.path, f.content]));
    expect(byPath["SKILL.md"]).toContain("name: web-skill");
    expect(byPath["reference.md"]).toBe("ref\n");
  });

  it("rejects file paths that escape the skill root", async () => {
    const root = await makeRoot();
    const storeRoot = join(root, "store");
    const manifest = `---\nname: escape\ndescription: test\n---\n# escape\n`;
    const files = [
      { path: "SKILL.md", bytes: Buffer.from(manifest, "utf8") },
      { path: "../escape.md", bytes: Buffer.from("x", "utf8") },
    ];
    await expect(importSkillFromFiles({ files, storeRoot })).rejects.toThrow(/must not escape/);
  });

  it("rejects duplicate file paths in importSkillFromFiles", async () => {
    const root = await makeRoot();
    const storeRoot = join(root, "store");
    const manifest = `---\nname: dup\ndescription: test\n---\n# dup\n`;
    const files = [
      { path: "SKILL.md", bytes: Buffer.from(manifest, "utf8") },
      { path: "SKILL.md", bytes: Buffer.from(manifest, "utf8") },
    ];
    await expect(importSkillFromFiles({ files, storeRoot })).rejects.toThrow(/duplicate skill file path/);
  });

  async function makeRoot(): Promise<string> {
    const root = await mkdtemp(join(tmpdir(), "cph-skill-store-"));
    roots.push(root);
    return root;
  }
});

async function makeSkill(root: string, name: string, description: string): Promise<string> {
  const source = join(root, "source", name);
  await mkdir(source, { recursive: true });
  await writeFile(join(source, "SKILL.md"), `---\nname: ${name}\ndescription: ${description}\n---\n# ${name}\n`);
  await writeFile(join(source, "reference.md"), "reference\n");
  return source;
}
