import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { akmClone } from "../src/commands/sources/source-clone";
import { saveConfig } from "../src/core/config/config";
import { UsageError } from "../src/core/errors";
import { type Cleanup, sandboxStashDir, sandboxXdgCacheHome, sandboxXdgConfigHome } from "./_helpers/sandbox";

const fixtureDirs: string[] = [];

function writeFile(filePath: string, content: string) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, content);
}

function createStashDir(prefix: string): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
  fixtureDirs.push(dir);
  for (const sub of ["skills", "commands", "agents", "knowledge", "scripts"]) {
    fs.mkdirSync(path.join(dir, sub), { recursive: true });
  }
  return dir;
}

let stashDir = "";
let searchPathDir = "";
let envCleanup: Cleanup = () => {};

beforeEach(() => {
  const cacheResult = sandboxXdgCacheHome();
  const cfgResult = sandboxXdgConfigHome(cacheResult.cleanup);
  const stashResult = sandboxStashDir(cfgResult.cleanup);
  stashDir = stashResult.dir;
  envCleanup = stashResult.cleanup;
  searchPathDir = createStashDir("akm-clone-searchpath-");

  saveConfig({
    semanticSearchMode: "off",
    sources: [{ type: "filesystem", path: searchPathDir }],
  });
});

afterEach(() => {
  envCleanup();
  envCleanup = () => {};
  stashDir = "";
  searchPathDir = "";
});

afterAll(() => {
  for (const dir of fixtureDirs.splice(0)) {
    if (dir) fs.rmSync(dir, { recursive: true, force: true });
  }
});

describe("akmClone", () => {
  test("clones a script from search path to primary stash", async () => {
    writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "#!/bin/bash\necho deploy\n");

    const result = await akmClone({ sourceRef: "script:deploy.sh" });

    expect(result.destination.ref).toContain("script:deploy.sh");
    expect(result.overwritten).toBe(false);
    expect(fs.existsSync(path.join(stashDir, "scripts", "deploy.sh"))).toBe(true);
    expect(fs.readFileSync(path.join(stashDir, "scripts", "deploy.sh"), "utf8")).toBe("#!/bin/bash\necho deploy\n");
  });

  test("clones a skill directory", async () => {
    writeFile(path.join(searchPathDir, "skills", "review", "SKILL.md"), "# Review Skill\n");
    writeFile(path.join(searchPathDir, "skills", "review", "helper.md"), "# Helper\n");

    const result = await akmClone({ sourceRef: "skill:review" });

    expect(result.overwritten).toBe(false);
    expect(fs.existsSync(path.join(stashDir, "skills", "review", "SKILL.md"))).toBe(true);
    expect(fs.existsSync(path.join(stashDir, "skills", "review", "helper.md"))).toBe(true);
  });

  test("clones with a new name", async () => {
    writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "echo deploy\n");

    const result = await akmClone({ sourceRef: "script:deploy.sh", newName: "my-deploy.sh" });

    expect(fs.existsSync(path.join(stashDir, "scripts", "my-deploy.sh"))).toBe(true);
    expect(result.destination.ref).toContain("my-deploy.sh");
  });

  test("throws when asset already exists without --force", async () => {
    writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "echo original\n");
    writeFile(path.join(stashDir, "scripts", "deploy.sh"), "echo existing\n");

    await expect(akmClone({ sourceRef: `${searchPathDir}//script:deploy.sh` })).rejects.toThrow("already exists");
  });

  test("overwrites with --force", async () => {
    writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "echo updated\n");
    writeFile(path.join(stashDir, "scripts", "deploy.sh"), "echo old\n");

    const result = await akmClone({ sourceRef: `${searchPathDir}//script:deploy.sh`, force: true });

    expect(result.overwritten).toBe(true);
    expect(fs.readFileSync(path.join(stashDir, "scripts", "deploy.sh"), "utf8")).toBe("echo updated\n");
  });

  test("force overwrite removes stale files from skill directory", async () => {
    // Source skill has only SKILL.md
    writeFile(path.join(searchPathDir, "skills", "review", "SKILL.md"), "# Updated\n");
    // Existing working skill has an extra file
    writeFile(path.join(stashDir, "skills", "review", "SKILL.md"), "# Old\n");
    writeFile(path.join(stashDir, "skills", "review", "stale.md"), "# Stale\n");

    await akmClone({ sourceRef: `${searchPathDir}//skill:review`, force: true });

    expect(fs.existsSync(path.join(stashDir, "skills", "review", "SKILL.md"))).toBe(true);
    expect(fs.existsSync(path.join(stashDir, "skills", "review", "stale.md"))).toBe(false);
  });

  test("throws when source asset not found", async () => {
    await expect(akmClone({ sourceRef: "script:nonexistent.sh" })).rejects.toThrow();
  });

  test("clones from working stash to itself with new name", async () => {
    writeFile(path.join(stashDir, "scripts", "original.sh"), "echo original\n");

    const _result = await akmClone({ sourceRef: "script:original.sh", newName: "copy.sh" });

    expect(fs.existsSync(path.join(stashDir, "scripts", "copy.sh"))).toBe(true);
  });

  test("throws when self-cloning a script without rename", async () => {
    writeFile(path.join(stashDir, "scripts", "deploy.sh"), "echo deploy\n");

    await expect(akmClone({ sourceRef: "script:deploy.sh" })).rejects.toThrow("same path");
    // Verify the file was not destroyed
    expect(fs.readFileSync(path.join(stashDir, "scripts", "deploy.sh"), "utf8")).toBe("echo deploy\n");
  });

  test("throws when self-cloning a skill without rename", async () => {
    writeFile(path.join(stashDir, "skills", "review", "SKILL.md"), "# Review\n");

    await expect(akmClone({ sourceRef: "skill:review" })).rejects.toThrow("same path");
    // Verify the skill was not destroyed
    expect(fs.existsSync(path.join(stashDir, "skills", "review", "SKILL.md"))).toBe(true);
  });

  describe("newName validation", () => {
    beforeEach(() => {
      writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "echo deploy\n");
      writeFile(path.join(searchPathDir, "skills", "review", "SKILL.md"), "# Review\n");
    });

    test("throws on empty string newName", async () => {
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: "" })).rejects.toThrow(UsageError);
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: "" })).rejects.toThrow(/empty/);
    });

    test("throws on '.' as newName", async () => {
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: "." })).rejects.toThrow(UsageError);
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: "." })).rejects.toThrow(/Unsafe/);
    });

    test("throws on '..' as newName", async () => {
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: ".." })).rejects.toThrow(UsageError);
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: ".." })).rejects.toThrow(/Unsafe/);
    });

    test("throws on path-traversal newName", async () => {
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: "../escape.sh" })).rejects.toThrow(UsageError);
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: "../escape.sh" })).rejects.toThrow(/Unsafe/);
    });

    test("throws on absolute path as newName", async () => {
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: "/etc/evil.sh" })).rejects.toThrow(UsageError);
      await expect(akmClone({ sourceRef: "script:deploy.sh", newName: "/etc/evil.sh" })).rejects.toThrow(/Unsafe/);
    });

    test("empty newName does not wipe the type directory for skills", async () => {
      writeFile(path.join(stashDir, "skills", "existing", "SKILL.md"), "# Existing\n");

      await expect(akmClone({ sourceRef: "skill:review", newName: "", force: true })).rejects.toThrow(UsageError);
      // Existing skill must be untouched
      expect(fs.existsSync(path.join(stashDir, "skills", "existing", "SKILL.md"))).toBe(true);
    });
  });
});

describe("akmClone --dest", () => {
  let customDest: string;

  beforeEach(() => {
    customDest = fs.mkdtempSync(path.join(os.tmpdir(), "akm-clone-dest-"));
  });

  afterEach(() => {
    if (customDest) fs.rmSync(customDest, { recursive: true, force: true });
  });

  test("clones script to custom destination preserving type dir structure", async () => {
    writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "#!/bin/bash\necho deploy\n");

    const result = await akmClone({ sourceRef: "script:deploy.sh", dest: customDest });

    expect(result.destination.path).toBe(path.join(customDest, "scripts", "deploy.sh"));
    expect(fs.existsSync(path.join(customDest, "scripts", "deploy.sh"))).toBe(true);
    expect(fs.readFileSync(path.join(customDest, "scripts", "deploy.sh"), "utf8")).toBe("#!/bin/bash\necho deploy\n");
    // Working stash should NOT have the file
    expect(fs.existsSync(path.join(stashDir, "scripts", "deploy.sh"))).toBe(false);
  });

  test("clones skill directory to custom destination", async () => {
    writeFile(path.join(searchPathDir, "skills", "review", "SKILL.md"), "# Review Skill\n");
    writeFile(path.join(searchPathDir, "skills", "review", "helper.md"), "# Helper\n");

    const result = await akmClone({ sourceRef: "skill:review", dest: customDest });

    expect(fs.existsSync(path.join(customDest, "skills", "review", "SKILL.md"))).toBe(true);
    expect(fs.existsSync(path.join(customDest, "skills", "review", "helper.md"))).toBe(true);
    expect(result.overwritten).toBe(false);
  });

  test("--dest does not require a working stash", async () => {
    writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "echo deploy\n");
    // Point AKM_STASH_DIR to a non-existent directory to simulate no working stash
    process.env.AKM_STASH_DIR = path.join(os.tmpdir(), `nonexistent-stash-${Date.now()}`);

    const result = await akmClone({
      sourceRef: `${searchPathDir}//script:deploy.sh`,
      dest: customDest,
    });

    expect(fs.existsSync(path.join(customDest, "scripts", "deploy.sh"))).toBe(true);
    expect(result.destination.path).toBe(path.join(customDest, "scripts", "deploy.sh"));
  });

  test("--dest with --force overwrites at custom destination", async () => {
    writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "echo updated\n");
    writeFile(path.join(customDest, "scripts", "deploy.sh"), "echo old\n");

    const result = await akmClone({
      sourceRef: `${searchPathDir}//script:deploy.sh`,
      force: true,
      dest: customDest,
    });

    expect(result.overwritten).toBe(true);
    expect(fs.readFileSync(path.join(customDest, "scripts", "deploy.sh"), "utf8")).toBe("echo updated\n");
  });

  test("throws when asset exists at --dest without --force", async () => {
    writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "echo new\n");
    writeFile(path.join(customDest, "scripts", "deploy.sh"), "echo existing\n");

    await expect(akmClone({ sourceRef: `${searchPathDir}//script:deploy.sh`, dest: customDest })).rejects.toThrow(
      "already exists at destination",
    );
  });
});

describe("akmClone remote", () => {
  let remoteFixtureDir: string;

  beforeEach(() => {
    // Create a fixture directory that simulates a remote package
    remoteFixtureDir = createStashDir("akm-clone-remote-fixture-");
    writeFile(path.join(remoteFixtureDir, "scripts", "remote-tool.sh"), "#!/bin/bash\necho remote\n");
    writeFile(path.join(remoteFixtureDir, "skills", "remote-skill", "SKILL.md"), "# Remote Skill\n");
  });

  afterEach(() => {
    if (remoteFixtureDir) fs.rmSync(remoteFixtureDir, { recursive: true, force: true });
  });

  test("clones a script from a remote origin via installRegistryRef", async () => {
    // Use bare path as origin — not in stashes, so isRemoteOrigin returns true
    const result = await akmClone({
      sourceRef: `${remoteFixtureDir}//script:remote-tool.sh`,
    });

    expect(result.remoteFetched).toBeDefined();
    expect(result.remoteFetched?.origin).toBe(remoteFixtureDir);
    expect(fs.existsSync(path.join(stashDir, "scripts", "remote-tool.sh"))).toBe(true);
    expect(fs.readFileSync(path.join(stashDir, "scripts", "remote-tool.sh"), "utf8")).toBe(
      "#!/bin/bash\necho remote\n",
    );
  });

  test("returns remoteFetched metadata", async () => {
    const result = await akmClone({
      sourceRef: `${remoteFixtureDir}//script:remote-tool.sh`,
    });

    expect(result.remoteFetched).toBeDefined();
    expect(result.remoteFetched?.stashRoot).toBeTruthy();
    expect(result.remoteFetched?.cacheDir).toBeTruthy();
  });

  test("throws when remote fetch succeeds but asset not found in package", async () => {
    await expect(akmClone({ sourceRef: `${remoteFixtureDir}//script:nonexistent.sh` })).rejects.toThrow("not found");
  });

  test("clones from remote origin to custom destination", async () => {
    const customDest = fs.mkdtempSync(path.join(os.tmpdir(), "akm-clone-remote-dest-"));
    try {
      const result = await akmClone({
        sourceRef: `${remoteFixtureDir}//script:remote-tool.sh`,
        dest: customDest,
      });

      expect(result.remoteFetched).toBeDefined();
      expect(fs.existsSync(path.join(customDest, "scripts", "remote-tool.sh"))).toBe(true);
      expect(result.destination.path).toBe(path.join(customDest, "scripts", "remote-tool.sh"));
    } finally {
      fs.rmSync(customDest, { recursive: true, force: true });
    }
  });
});
