import { vi, describe, it, expect, beforeEach } from "vitest";
import { join } from "node:path";

// ─── Hoisted mocks ───────────────────────────────────────────────────────────

const mockHomedir = vi.hoisted(() => {
  let dir = "/fake/home";
  return { get: () => dir, set: (d: string) => { dir = d; } };
});

/** Build the expected worktree directory path using platform-aware join(). */
function expectedWorktreePath(repoName: string, sanitizedBranch: string): string {
  return join(mockHomedir.get(), ".zencode", "worktrees", repoName, sanitizedBranch);
}

/** Build the expected worktrees parent directory using platform-aware join(). */
function expectedWorktreesParent(repoName: string): string {
  return join(mockHomedir.get(), ".zencode", "worktrees", repoName);
}

const mockBunSpawn = vi.hoisted(() => vi.fn());
const mockExistsSync = vi.hoisted(() => vi.fn());
const mockMkdirSync = vi.hoisted(() => vi.fn());
const mockEnsureSecureDir = vi.hoisted(() => vi.fn());

vi.mock("node:os", () => ({ homedir: () => mockHomedir.get() }));
vi.stubGlobal("Bun", { spawn: mockBunSpawn });
vi.mock("node:fs", () => ({
  existsSync: mockExistsSync,
  mkdirSync: mockMkdirSync,
}));
vi.mock("./fs-permissions.js", () => ({
  ensureSecureDir: mockEnsureSecureDir,
}));

// ─── Helpers ─────────────────────────────────────────────────────────────────

/** Create a mock Bun.spawn process result. */
function createMockProc(stdout = "", exitCode = 0, stderr = "") {
  return {
    stdout: new ReadableStream({
      start(controller) {
        controller.enqueue(new TextEncoder().encode(stdout));
        controller.close();
      },
    }),
    stderr: new ReadableStream({
      start(controller) {
        controller.enqueue(new TextEncoder().encode(stderr));
        controller.close();
      },
    }),
    exited: Promise.resolve(exitCode),
    kill: vi.fn(),
  };
}

/** Reconstruct a full command string from Bun.spawn call args for pattern matching. */
function cmdFromCall(call: unknown[]): string {
  return (call[0] as string[]).join(" ");
}

function mockGitCommand(pattern: string | RegExp, result: string) {
  mockBunSpawn.mockImplementation((cmd: string[]) => {
    const cmdStr = cmd.join(" ");
    if (typeof pattern === "string" ? cmdStr.includes(pattern) : pattern.test(cmdStr)) {
      return createMockProc(result);
    }
    return createMockProc("", 1, `Unexpected git command: ${cmdStr}`);
  });
}

function mockGitCommands(map: Record<string, string | Error>) {
  mockBunSpawn.mockImplementation((cmd: string[]) => {
    const cmdStr = cmd.join(" ");
    for (const [pattern, result] of Object.entries(map)) {
      if (cmdStr.includes(pattern)) {
        if (result instanceof Error) return createMockProc("", 1, result.message);
        return createMockProc(result);
      }
    }
    return createMockProc("", 1, `Unmocked git command: ${cmdStr}`);
  });
}

// ─── Dynamic import with module reset ────────────────────────────────────────

let gitUtils: typeof import("./git-utils.js");

beforeEach(async () => {
  vi.resetModules();
  mockBunSpawn.mockReset();
  mockExistsSync.mockReset();
  mockMkdirSync.mockReset();
  mockEnsureSecureDir.mockReset();
  mockHomedir.set("/fake/home");
  gitUtils = await import("./git-utils.js");
});

// ─── getRepoInfo ─────────────────────────────────────────────────────────────

describe("getRepoInfo", () => {
  it("returns null for a non-git directory", async () => {
    mockBunSpawn.mockImplementation(() => createMockProc("", 1, "fatal: not a git repository"));

    const result = await gitUtils.getRepoInfo("/tmp/not-a-repo");
    expect(result).toBeNull();
  });

  it("returns correct repo info for a standard git repo", async () => {
    mockGitCommands({
      "rev-parse --show-toplevel": "/home/user/my-project",
      "rev-parse --abbrev-ref HEAD": "feat/cool-feature",
      "rev-parse --git-dir": ".git",
      "symbolic-ref refs/remotes/origin/HEAD": "refs/remotes/origin/main",
    });

    const result = await gitUtils.getRepoInfo("/home/user/my-project");
    expect(result).toEqual({
      repoRoot: "/home/user/my-project",
      repoName: "my-project",
      currentBranch: "feat/cool-feature",
      defaultBranch: "main",
      isWorktree: false,
    });
  });

  it("detects worktree when git-dir contains /worktrees/", async () => {
    mockGitCommands({
      "rev-parse --show-toplevel": "/fake/home/.zencode/worktrees/proj/feat--x",
      "rev-parse --abbrev-ref HEAD": "feat/x",
      "rev-parse --git-dir": "/home/user/proj/.git/worktrees/feat--x",
      "symbolic-ref refs/remotes/origin/HEAD": "refs/remotes/origin/main",
    });

    const result = await gitUtils.getRepoInfo("/fake/home/.zencode/worktrees/proj/feat--x");
    expect(result).not.toBeNull();
    expect(result!.isWorktree).toBe(true);
  });

  it("falls back to 'HEAD' when branch detection fails", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("rev-parse --show-toplevel")) return createMockProc("/repo");
      if (cmdStr.includes("rev-parse --abbrev-ref HEAD")) return createMockProc("", 1, "detached HEAD");
      if (cmdStr.includes("rev-parse --git-dir")) return createMockProc(".git");
      if (cmdStr.includes("symbolic-ref refs/remotes/origin/HEAD")) return createMockProc("refs/remotes/origin/main");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.getRepoInfo("/repo");
    expect(result).not.toBeNull();
    expect(result!.currentBranch).toBe("HEAD");
  });

  it("resolves default branch via origin HEAD", async () => {
    mockGitCommands({
      "rev-parse --show-toplevel": "/repo",
      "rev-parse --abbrev-ref HEAD": "develop",
      "rev-parse --git-dir": ".git",
      "symbolic-ref refs/remotes/origin/HEAD": "refs/remotes/origin/develop",
    });

    const result = await gitUtils.getRepoInfo("/repo");
    expect(result!.defaultBranch).toBe("develop");
  });

  it("falls back to 'main' when origin HEAD and master are unavailable", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("rev-parse --show-toplevel")) return createMockProc("/repo");
      if (cmdStr.includes("rev-parse --abbrev-ref HEAD")) return createMockProc("feature");
      if (cmdStr.includes("rev-parse --git-dir")) return createMockProc(".git");
      if (cmdStr.includes("symbolic-ref refs/remotes/origin/HEAD")) return createMockProc("", 1, "no origin");
      if (cmdStr.includes("branch --list main master")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.getRepoInfo("/repo");
    expect(result!.defaultBranch).toBe("main");
  });

  it("falls back to 'master' when origin HEAD fails and only master exists", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("rev-parse --show-toplevel")) return createMockProc("/repo");
      if (cmdStr.includes("rev-parse --abbrev-ref HEAD")) return createMockProc("feature");
      if (cmdStr.includes("rev-parse --git-dir")) return createMockProc(".git");
      if (cmdStr.includes("symbolic-ref refs/remotes/origin/HEAD")) return createMockProc("", 1, "no origin");
      if (cmdStr.includes("branch --list main master")) return createMockProc("  master");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.getRepoInfo("/repo");
    expect(result!.defaultBranch).toBe("master");
  });
});

// ─── listBranches ────────────────────────────────────────────────────────────

describe("listBranches", () => {
  it("parses local branches with current marker", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc("");
      if (cmdStr.includes("for-each-ref") && cmdStr.includes("refs/heads/")) {
        return createMockProc("main\t*\nfeat/login\t ");
      }
      if (cmdStr.includes("for-each-ref") && cmdStr.includes("refs/remotes/origin/")) return createMockProc("");
      if (cmdStr.includes("rev-list --left-right --count")) return createMockProc("0\t0");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const branches = await gitUtils.listBranches("/repo");
    const main = branches.find((b) => b.name === "main");
    const feat = branches.find((b) => b.name === "feat/login");

    expect(main).toBeDefined();
    expect(main!.isCurrent).toBe(true);
    expect(main!.isRemote).toBe(false);

    expect(feat).toBeDefined();
    expect(feat!.isCurrent).toBe(false);
  });

  it("includes remote-only branches", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc("");
      if (cmdStr.includes("for-each-ref") && cmdStr.includes("refs/heads/")) {
        return createMockProc("main\t*");
      }
      if (cmdStr.includes("for-each-ref") && cmdStr.includes("refs/remotes/origin/")) {
        return createMockProc("origin/feat/remote-branch");
      }
      if (cmdStr.includes("rev-list --left-right --count")) return createMockProc("0\t0");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const branches = await gitUtils.listBranches("/repo");
    const remote = branches.find((b) => b.name === "feat/remote-branch");

    expect(remote).toBeDefined();
    expect(remote!.isRemote).toBe(true);
    expect(remote!.isCurrent).toBe(false);
    expect(remote!.ahead).toBe(0);
    expect(remote!.behind).toBe(0);
  });

  it("excludes origin/HEAD from remote branches", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc("");
      if (cmdStr.includes("for-each-ref") && cmdStr.includes("refs/heads/")) return createMockProc("");
      if (cmdStr.includes("for-each-ref") && cmdStr.includes("refs/remotes/origin/")) {
        return createMockProc("origin/HEAD\norigin/main");
      }
      if (cmdStr.includes("rev-list --left-right --count")) return createMockProc("0\t0");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const branches = await gitUtils.listBranches("/repo");
    expect(branches.find((b) => b.name === "HEAD")).toBeUndefined();
    expect(branches.find((b) => b.name === "main")).toBeDefined();
  });

  it("includes ahead/behind counts for local branches", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc("");
      if (cmdStr.includes("for-each-ref") && cmdStr.includes("refs/heads/")) {
        return createMockProc("dev\t ");
      }
      if (cmdStr.includes("for-each-ref") && cmdStr.includes("refs/remotes/origin/")) return createMockProc("");
      if (cmdStr.includes("rev-list --left-right --count")) return createMockProc("3\t5");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const branches = await gitUtils.listBranches("/repo");
    const dev = branches.find((b) => b.name === "dev");
    expect(dev).toBeDefined();
    // In the source: [behind, ahead] = raw.split(...).map(Number)
    expect(dev!.ahead).toBe(5);
    expect(dev!.behind).toBe(3);
  });

  it("returns empty array on git failure", async () => {
    mockBunSpawn.mockImplementation(() => createMockProc("", 1, "git failed"));

    const branches = await gitUtils.listBranches("/repo");
    expect(branches).toEqual([]);
  });
});

// ─── listWorktrees ───────────────────────────────────────────────────────────

describe("listWorktrees", () => {
  it("parses porcelain output correctly", async () => {
    const porcelain = [
      "worktree /home/user/project",
      "HEAD abc1234567890abcdef1234567890abcdef123456",
      "branch refs/heads/main",
      "",
      "worktree /fake/home/.zencode/worktrees/project/feat--x",
      "HEAD def4567890abcdef1234567890abcdef12345678",
      "branch refs/heads/feat/x",
      "",
    ].join("\n");

    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc(porcelain);
      // isWorktreeDirty calls
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    mockExistsSync.mockReturnValue(true);

    const worktrees = await gitUtils.listWorktrees("/home/user/project");
    expect(worktrees).toHaveLength(2);
    expect(worktrees[0].path).toBe("/home/user/project");
    expect(worktrees[1].path).toBe("/fake/home/.zencode/worktrees/project/feat--x");
  });

  it("marks first worktree as main", async () => {
    const porcelain = [
      "worktree /home/user/project",
      "HEAD abc123",
      "branch refs/heads/main",
      "",
      "worktree /tmp/wt",
      "HEAD def456",
      "branch refs/heads/other",
      "",
    ].join("\n");

    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc(porcelain);
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    mockExistsSync.mockReturnValue(true);

    const worktrees = await gitUtils.listWorktrees("/home/user/project");
    expect(worktrees[0].isMainWorktree).toBe(true);
    expect(worktrees[1].isMainWorktree).toBe(false);
  });

  it("strips refs/heads/ from branch names", async () => {
    const porcelain = [
      "worktree /repo",
      "HEAD abc123",
      "branch refs/heads/feat/something",
      "",
    ].join("\n");

    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc(porcelain);
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    mockExistsSync.mockReturnValue(true);

    const worktrees = await gitUtils.listWorktrees("/repo");
    expect(worktrees[0].branch).toBe("feat/something");
  });

  it("returns empty array on failure", async () => {
    mockBunSpawn.mockImplementation(() => createMockProc("", 1, "git failed"));

    const worktrees = await gitUtils.listWorktrees("/repo");
    expect(worktrees).toEqual([]);
  });
});

// ─── ensureWorktree ──────────────────────────────────────────────────────────

describe("ensureWorktree", () => {
  it("returns existing worktree without creating a new one", async () => {
    const porcelain = [
      "worktree /repo",
      "HEAD abc123",
      "branch refs/heads/main",
      "",
      "worktree /existing/path",
      "HEAD def456",
      "branch refs/heads/feat/existing",
      "",
    ].join("\n");

    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc(porcelain);
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    mockExistsSync.mockReturnValue(true);

    const result = await gitUtils.ensureWorktree("/repo", "feat/existing");
    expect(result.worktreePath).toBe("/existing/path");
    expect(result.branch).toBe("feat/existing");
    expect(result.actualBranch).toBe("feat/existing");
    expect(result.isNew).toBe(false);
    // Should NOT have called worktree add
    const addCalls = mockBunSpawn.mock.calls.filter((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree add"),
    );
    expect(addCalls).toHaveLength(0);
  });

  it("creates worktree for an existing local branch", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      // listWorktrees
      if (cmdStr.includes("worktree list --porcelain")) {
        return createMockProc("worktree /repo\nHEAD abc\nbranch refs/heads/main\n");
      }
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      // Branch exists locally
      if (cmdStr.includes("rev-parse --verify refs/heads/feat/local")) return createMockProc("abc123");
      // worktree add
      if (cmdStr.includes("worktree add")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    // Target path doesn't exist yet (no suffix needed)
    mockExistsSync.mockReturnValue(false);

    const result = await gitUtils.ensureWorktree("/repo", "feat/local");
    expect(result.worktreePath).toBe(expectedWorktreePath("repo", "feat--local"));
    expect(result.actualBranch).toBe("feat/local");
    expect(result.isNew).toBe(false);

    const addCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree add"),
    );
    expect(addCall).toBeDefined();
    // Should NOT have -b flag for existing branch
    expect(cmdFromCall(addCall! as unknown[])).not.toContain("-b ");
  });

  it("creates tracking branch from remote", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) {
        return createMockProc("worktree /repo\nHEAD abc\nbranch refs/heads/main\n");
      }
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      // Local branch does NOT exist
      if (cmdStr.includes("rev-parse --verify refs/heads/feat/remote"))
        return createMockProc("", 1, "not found");
      // Remote branch exists
      if (cmdStr.includes("rev-parse --verify refs/remotes/origin/feat/remote"))
        return createMockProc("def456");
      // worktree add -b
      if (cmdStr.includes("worktree add -b")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    // Target path doesn't exist yet
    mockExistsSync.mockReturnValue(false);

    const result = await gitUtils.ensureWorktree("/repo", "feat/remote");
    expect(result.actualBranch).toBe("feat/remote");
    expect(result.isNew).toBe(false);

    const addCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree add -b"),
    );
    expect(addCall).toBeDefined();
    expect(cmdFromCall(addCall! as unknown[])).toContain("origin/feat/remote");
  });

  it("creates new branch from origin/base when branch does not exist anywhere", async () => {
    // When neither the requested branch nor its remote counterpart exist,
    // but origin/{baseBranch} is available (after fetch), use origin/{baseBranch}
    // as the start point instead of the potentially stale local ref.
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) {
        return createMockProc("worktree /repo\nHEAD abc\nbranch refs/heads/main\n");
      }
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      // Neither local nor remote branch exists for feat/new
      if (cmdStr.includes("rev-parse --verify refs/heads/feat/new")) return createMockProc("", 1, "not found");
      if (cmdStr.includes("rev-parse --verify refs/remotes/origin/feat/new")) return createMockProc("", 1, "not found");
      // Remote ref for the base branch exists (up-to-date after fetch)
      if (cmdStr.includes("rev-parse --verify refs/remotes/origin/develop")) return createMockProc("abc123");
      // worktree add -b
      if (cmdStr.includes("worktree add -b")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    // Target path doesn't exist yet
    mockExistsSync.mockReturnValue(false);

    const result = await gitUtils.ensureWorktree("/repo", "feat/new", { baseBranch: "develop" });
    expect(result.isNew).toBe(true);
    expect(result.branch).toBe("feat/new");
    expect(result.actualBranch).toBe("feat/new");

    const addCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree add -b"),
    );
    expect(addCall).toBeDefined();
    // Should use origin/develop (remote ref), NOT local develop
    expect(cmdFromCall(addCall! as unknown[])).toContain("origin/develop");
  });

  it("falls back to local base branch when origin ref does not exist", async () => {
    // When origin/{baseBranch} is not available (e.g. no remote), fall back
    // to the local base branch ref.
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) {
        return createMockProc("worktree /repo\nHEAD abc\nbranch refs/heads/main\n");
      }
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      // Neither local nor remote branch exists for feat/new, and no origin/develop
      if (cmdStr.includes("rev-parse --verify")) return createMockProc("", 1, "not found");
      // worktree add -b
      if (cmdStr.includes("worktree add -b")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    // Target path doesn't exist yet
    mockExistsSync.mockReturnValue(false);

    const result = await gitUtils.ensureWorktree("/repo", "feat/new", { baseBranch: "develop" });
    expect(result.isNew).toBe(true);

    const addCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree add -b"),
    );
    expect(addCall).toBeDefined();
    // Should fall back to local "develop" since origin/develop doesn't exist
    expect(cmdFromCall(addCall! as unknown[])).toContain("develop");
    expect(cmdFromCall(addCall! as unknown[])).not.toContain("origin/develop");
  });

  it("throws when createBranch=false and branch does not exist", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) {
        return createMockProc("worktree /repo\nHEAD abc\nbranch refs/heads/main\n");
      }
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      if (cmdStr.includes("rev-parse --verify")) return createMockProc("", 1, "not found");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    // Target path doesn't exist yet
    mockExistsSync.mockReturnValue(false);

    await expect(
      gitUtils.ensureWorktree("/repo", "feat/missing", { createBranch: false }),
    ).rejects.toThrow('Branch "feat/missing" does not exist and createBranch is false');
  });

  it("calls ensureSecureDir when creating worktree", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) {
        return createMockProc("worktree /repo\nHEAD abc\nbranch refs/heads/main\n");
      }
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      if (cmdStr.includes("rev-parse --verify refs/heads/feat/new")) return createMockProc("abc");
      if (cmdStr.includes("worktree add")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    // Target path doesn't exist yet
    mockExistsSync.mockReturnValue(false);

    await gitUtils.ensureWorktree("/repo", "feat/new");

    expect(mockEnsureSecureDir).toHaveBeenCalledWith(
      expectedWorktreesParent("repo"),
    );
  });

  it("does not reuse the main worktree even when branch matches", async () => {
    // Main worktree is on "main", and we request a worktree for "main"
    const porcelain = [
      "worktree /repo",
      "HEAD abc123",
      "branch refs/heads/main",
      "",
    ].join("\n");

    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc(porcelain);
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      if (cmdStr.includes("rev-parse HEAD")) return createMockProc("abc123");
      // generateUniqueWorktreeBranch checks for existing branches (random suffix)
      if (/rev-parse --verify refs\/heads\/main-wt-\d{4}/.test(cmdStr)) return createMockProc("", 1, "not found");
      if (cmdStr.includes("worktree add -b")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    // Target path doesn't exist yet
    mockExistsSync.mockReturnValue(false);

    const result = await gitUtils.ensureWorktree("/repo", "main");
    // Should NOT return the main repo path
    expect(result.worktreePath).not.toBe("/repo");
    expect(result.worktreePath).toBe(expectedWorktreePath("repo", "main"));
    expect(result.branch).toBe("main");
    expect(result.actualBranch).toMatch(/^main-wt-\d{4}$/);
    // Should create a branch-tracking worktree
    const addCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree add -b"),
    );
    expect(addCall).toBeDefined();
    expect(cmdFromCall(addCall! as unknown[])).toMatch(/main-wt-\d{4}/);
    expect(cmdFromCall(addCall! as unknown[])).toContain("abc123");
  });

  it("creates unique paths with random suffix when base path exists", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) {
        return createMockProc("worktree /repo\nHEAD abc\nbranch refs/heads/main\n");
      }
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      if (cmdStr.includes("rev-parse --verify refs/heads/feat/x")) return createMockProc("abc123");
      if (cmdStr.includes("worktree add")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    // Base path exists, random suffix path does not
    const basePath = expectedWorktreePath("repo", "feat--x");
    mockExistsSync.mockImplementation((path: string) => {
      if (path === basePath) return true;
      return false; // Any random-suffixed path is free
    });

    const result = await gitUtils.ensureWorktree("/repo", "feat/x");
    expect(result.worktreePath).toMatch(new RegExp(`^${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-\\d{4}$`));
  });

  it("creates branch-tracking worktree when forceNew=true and worktree already exists", async () => {
    const porcelain = [
      "worktree /repo",
      "HEAD abc123",
      "branch refs/heads/main",
      "",
      "worktree /existing/wt",
      "HEAD def456",
      "branch refs/heads/feat/existing",
      "",
    ].join("\n");

    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc(porcelain);
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      if (cmdStr.includes("rev-parse HEAD")) return createMockProc("def456");
      // generateUniqueWorktreeBranch checks (random suffix)
      if (/rev-parse --verify refs\/heads\/feat\/existing-wt-\d{4}/.test(cmdStr)) return createMockProc("", 1, "not found");
      if (cmdStr.includes("worktree add -b")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    // Target path doesn't exist yet
    mockExistsSync.mockReturnValue(false);

    const result = await gitUtils.ensureWorktree("/repo", "feat/existing", { forceNew: true });
    expect(result.worktreePath).toBe(expectedWorktreePath("repo", "feat--existing"));
    expect(result.branch).toBe("feat/existing");
    expect(result.actualBranch).toMatch(/^feat\/existing-wt-\d{4}$/);

    const addCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree add -b"),
    );
    expect(addCall).toBeDefined();
    expect(cmdFromCall(addCall! as unknown[])).toMatch(/feat\/existing-wt-\d{4}/);
  });

  it("generates unique branch when forceNew=true and branch exists locally but no worktree uses it", async () => {
    // Main repo is on a different branch (feat/other), not on "main"
    const porcelain = [
      "worktree /repo",
      "HEAD abc123",
      "branch refs/heads/feat/other",
      "",
    ].join("\n");

    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc(porcelain);
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      // "main" exists as a local branch
      if (cmdStr.includes("rev-parse --verify refs/heads/main") && !cmdStr.includes("-wt-")) return createMockProc("aaa111");
      // rev-parse for the commit hash (git() uses cwd, not -C)
      if (cmdStr === "git rev-parse refs/heads/main") return createMockProc("aaa111");
      // generateUniqueWorktreeBranch checks (random suffix)
      if (/rev-parse --verify refs\/heads\/main-wt-\d{4}/.test(cmdStr)) return createMockProc("", 1, "not found");
      if (cmdStr.includes("worktree add -b")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    mockExistsSync.mockReturnValue(false);

    const result = await gitUtils.ensureWorktree("/repo", "main", { forceNew: true });
    expect(result.worktreePath).toBe(expectedWorktreePath("repo", "main"));
    expect(result.branch).toBe("main");
    // Should get a unique branch, NOT the raw "main" branch
    expect(result.actualBranch).toMatch(/^main-wt-\d{4}$/);

    const addCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree add -b"),
    );
    expect(addCall).toBeDefined();
    expect(cmdFromCall(addCall! as unknown[])).toMatch(/main-wt-\d{4}/);
  });

  it("generates unique branch when forceNew=true and only remote branch exists", async () => {
    // No worktree on "main", main repo on different branch
    const porcelain = [
      "worktree /repo",
      "HEAD abc123",
      "branch refs/heads/feat/other",
      "",
    ].join("\n");

    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("worktree list --porcelain")) return createMockProc(porcelain);
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      // "main" does NOT exist locally
      if (cmdStr.includes("rev-parse --verify refs/heads/main") && !cmdStr.includes("-wt-") && !cmdStr.includes("remotes")) return createMockProc("", 1, "not found");
      // "main" exists on remote
      if (cmdStr.includes("rev-parse --verify refs/remotes/origin/main")) return createMockProc("bbb222");
      // generateUniqueWorktreeBranch checks
      if (/rev-parse --verify refs\/heads\/main-wt-\d{4}/.test(cmdStr)) return createMockProc("", 1, "not found");
      if (cmdStr.includes("worktree add -b")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });
    mockExistsSync.mockReturnValue(false);

    const result = await gitUtils.ensureWorktree("/repo", "main", { forceNew: true });
    expect(result.branch).toBe("main");
    expect(result.actualBranch).toMatch(/^main-wt-\d{4}$/);

    const addCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree add -b"),
    );
    expect(addCall).toBeDefined();
    expect(cmdFromCall(addCall! as unknown[])).toMatch(/main-wt-\d{4}/);
    expect(cmdFromCall(addCall! as unknown[])).toContain("origin/main");
  });
});

// ─── generateUniqueWorktreeBranch ────────────────────────────────────────────

describe("generateUniqueWorktreeBranch", () => {
  it("returns branch-wt-{random4digit} when no suffixed branches exist", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("rev-parse --verify refs/heads/main-wt-")) return createMockProc("", 1, "not found");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.generateUniqueWorktreeBranch("/repo", "main");
    expect(result).toMatch(/^main-wt-\d{4}$/);
  });

  it("retries with a new random suffix on collision", async () => {
    // Mock Math.random to return deterministic values
    const origRandom = Math.random;
    const randomValues = [0.5, 0.7]; // → suffixes 5500, 7300
    let callIdx = 0;
    Math.random = () => randomValues[callIdx++] ?? origRandom();

    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      // First candidate (5500) already exists
      if (cmdStr.includes("rev-parse --verify refs/heads/feat/x-wt-5500")) return createMockProc("abc");
      // Second candidate (7300) is free
      if (cmdStr.includes("rev-parse --verify refs/heads/feat/x-wt-7300")) return createMockProc("", 1, "not found");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.generateUniqueWorktreeBranch("/repo", "feat/x");
    expect(result).toBe("feat/x-wt-7300");

    Math.random = origRandom;
  });
});

// ─── removeWorktree ──────────────────────────────────────────────────────────

describe("removeWorktree", () => {
  it("prunes when worktree path does not exist on disk", async () => {
    mockExistsSync.mockReturnValue(false);
    mockGitCommand("worktree prune", "");

    const result = await gitUtils.removeWorktree("/repo", "/gone/path");
    expect(result.removed).toBe(true);
    expect(result.reason).toBeUndefined();

    const pruneCalls = mockBunSpawn.mock.calls.filter((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("worktree prune"),
    );
    expect(pruneCalls).toHaveLength(1);
  });

  it("deletes branchToDelete after pruning a missing worktree", async () => {
    mockExistsSync.mockReturnValue(false);
    mockGitCommands({
      "worktree prune": "",
      "branch -D main-wt-2": "",
    });

    const result = await gitUtils.removeWorktree("/repo", "/gone/path", { branchToDelete: "main-wt-2" });
    expect(result.removed).toBe(true);

    const branchDeleteCalls = mockBunSpawn.mock.calls.filter((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("branch -D main-wt-2"),
    );
    expect(branchDeleteCalls).toHaveLength(1);
  });

  it("deletes branchToDelete after successful worktree removal", async () => {
    mockExistsSync.mockReturnValue(true);
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      if (cmdStr.includes("worktree remove")) return createMockProc("");
      if (cmdStr.includes("branch -D feat-wt-3")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.removeWorktree("/repo", "/wt/path", { branchToDelete: "feat-wt-3" });
    expect(result.removed).toBe(true);

    const branchDeleteCalls = mockBunSpawn.mock.calls.filter((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("branch -D feat-wt-3"),
    );
    expect(branchDeleteCalls).toHaveLength(1);
  });

  it("refuses to remove dirty worktree without force", async () => {
    mockExistsSync.mockReturnValue(true);
    mockGitCommand("status --porcelain", " M dirty-file.ts");

    const result = await gitUtils.removeWorktree("/repo", "/wt/path");
    expect(result.removed).toBe(false);
    expect(result.reason).toContain("uncommitted changes");
  });

  it("force-removes dirty worktree", async () => {
    // existsSync: first call for removeWorktree check, second for isWorktreeDirty
    mockExistsSync.mockReturnValue(true);
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("status --porcelain")) return createMockProc(" M dirty.ts");
      if (cmdStr.includes("worktree remove") && cmdStr.includes("--force")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.removeWorktree("/repo", "/wt/path", { force: true });
    expect(result.removed).toBe(true);
  });

  it("returns reason on error during removal", async () => {
    mockExistsSync.mockReturnValue(true);
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("status --porcelain")) return createMockProc("");
      if (cmdStr.includes("worktree remove"))
        return createMockProc("", 1, "worktree is locked");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.removeWorktree("/repo", "/wt/path");
    expect(result.removed).toBe(false);
    expect(result.reason).toContain("worktree is locked");
  });
});

// ─── isWorktreeDirty ─────────────────────────────────────────────────────────

describe("isWorktreeDirty", () => {
  it("returns false when path does not exist", async () => {
    mockExistsSync.mockReturnValue(false);

    expect(await gitUtils.isWorktreeDirty("/nonexistent")).toBe(false);
  });

  it("returns false when status is empty", async () => {
    mockExistsSync.mockReturnValue(true);
    mockGitCommand("status --porcelain", "");

    expect(await gitUtils.isWorktreeDirty("/clean/repo")).toBe(false);
  });

  it("returns true when status has output", async () => {
    mockExistsSync.mockReturnValue(true);
    mockGitCommand("status --porcelain", " M file.ts\n?? new-file.ts");

    expect(await gitUtils.isWorktreeDirty("/dirty/repo")).toBe(true);
  });
});

// ─── getBranchStatus ─────────────────────────────────────────────────────────

describe("getBranchStatus", () => {
  it("parses ahead/behind counts correctly", async () => {
    mockGitCommand("rev-list --left-right --count", "7\t12");

    const status = await gitUtils.getBranchStatus("/repo", "feat/branch");
    // Source: [behind, ahead] = raw.split(...).map(Number)
    expect(status.ahead).toBe(12);
    expect(status.behind).toBe(7);
  });

  it("returns 0/0 when there is no upstream", async () => {
    mockBunSpawn.mockImplementation(() => {
      return createMockProc("", 1, "no upstream configured");
    });

    const status = await gitUtils.getBranchStatus("/repo", "local-only");
    expect(status.ahead).toBe(0);
    expect(status.behind).toBe(0);
  });

  it("handles zero ahead/behind", async () => {
    mockGitCommand("rev-list --left-right --count", "0\t0");

    const status = await gitUtils.getBranchStatus("/repo", "main");
    expect(status.ahead).toBe(0);
    expect(status.behind).toBe(0);
  });
});

// ─── checkoutOrCreateBranch ─────────────────────────────────────────────────

describe("checkoutOrCreateBranch", () => {
  it("checks out an existing branch without creating", async () => {
    mockGitCommand("checkout feat/existing", "Switched to branch 'feat/existing'");

    const result = await gitUtils.checkoutOrCreateBranch("/repo", "feat/existing");
    expect(result.created).toBe(false);
  });

  it("creates branch from origin/defaultBranch when checkout fails and createBranch=true", async () => {
    // Checkout fails (branch doesn't exist), but origin/main is available
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("checkout feat/new") && !cmdStr.includes("-b"))
        return createMockProc("", 1, "error: pathspec 'feat/new' did not match any file(s) known to git");
      if (cmdStr.includes("rev-parse --verify refs/remotes/origin/main")) return createMockProc("abc123");
      if (cmdStr.includes("checkout -b feat/new origin/main")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.checkoutOrCreateBranch("/repo", "feat/new", {
      createBranch: true,
      defaultBranch: "main",
    });
    expect(result.created).toBe(true);

    // Verify the correct git command was called
    const createCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("checkout -b"),
    );
    expect(createCall).toBeDefined();
    expect(cmdFromCall(createCall! as unknown[])).toContain("origin/main");
  });

  it("falls back to local defaultBranch when origin ref does not exist", async () => {
    // Checkout fails, and origin/main is not available either
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("checkout feat/new") && !cmdStr.includes("-b"))
        return createMockProc("", 1, "error: pathspec 'feat/new' did not match any file(s) known to git");
      if (cmdStr.includes("rev-parse --verify refs/remotes/origin/main"))
        return createMockProc("", 1, "not found");
      if (cmdStr.includes("checkout -b feat/new main")) return createMockProc("");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    const result = await gitUtils.checkoutOrCreateBranch("/repo", "feat/new", {
      createBranch: true,
      defaultBranch: "main",
    });
    expect(result.created).toBe(true);

    const createCall = mockBunSpawn.mock.calls.find((c: unknown[]) =>
      cmdFromCall(c as unknown[]).includes("checkout -b"),
    );
    expect(createCall).toBeDefined();
    expect(cmdFromCall(createCall! as unknown[])).toContain("main");
    expect(cmdFromCall(createCall! as unknown[])).not.toContain("origin/main");
  });

  it("throws when branch does not exist and createBranch is not set", async () => {
    mockBunSpawn.mockImplementation((cmd: string[]) => {
      const cmdStr = cmd.join(" ");
      if (cmdStr.includes("checkout feat/missing"))
        return createMockProc("", 1, "error: pathspec 'feat/missing' did not match any file(s) known to git");
      return createMockProc("", 1, `Unmocked: ${cmdStr}`);
    });

    await expect(
      gitUtils.checkoutOrCreateBranch("/repo", "feat/missing"),
    ).rejects.toThrow('Branch "feat/missing" does not exist');
  });
});
