//! Git worktree management

use anyhow::{Context, Result, bail};
use git2::Repository;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use tracing::{debug, info, warn};

const LOCAL_INSTRUCTION_FILE_NAMES: &[&str] = &["AGENTS.md", "CLAUDE.md"];

fn remove_dir_all_with_retries(path: &Path) -> Result<()> {
    if !path.exists() {
        return Ok(());
    }

    let mut last_err = None;
    for attempt in 0u64..10 {
        match fs::remove_dir_all(path) {
            Ok(()) => {
                last_err = None;
                break;
            }
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
                last_err = None;
                break;
            }
            Err(e) => {
                last_err = Some(e);
            }
        }

        std::thread::sleep(std::time::Duration::from_millis(100 * (attempt + 1)));

        if !path.exists() {
            last_err = None;
            break;
        }
    }

    if path.exists() {
        let Some(e) = last_err else {
            bail!("Failed to remove directory at {}", path.display());
        };
        bail!("Failed to remove directory at {}: {e}", path.display());
    }

    Ok(())
}

fn is_empty_dir(path: &Path) -> Result<bool> {
    let mut entries = fs::read_dir(path)
        .with_context(|| format!("Failed to read directory {}", path.display()))?;
    Ok(entries.next().is_none())
}

fn normalize_ignored_rel_path(raw_path: &[u8]) -> Option<PathBuf> {
    use std::ffi::OsStr;
    use std::path::Component;

    if raw_path.is_empty() {
        return None;
    }

    let mut rel = String::from_utf8_lossy(raw_path).into_owned();
    rel.truncate(rel.trim_end_matches('/').len());

    if rel.is_empty() {
        return None;
    }

    let rel_path = PathBuf::from(rel);
    if rel_path.is_absolute()
        || rel_path.components().any(|c| {
            matches!(
                c,
                Component::Prefix(_) | Component::RootDir | Component::ParentDir
            )
        })
    {
        return None;
    }

    let Some(Component::Normal(top)) = rel_path.components().next() else {
        return None;
    };
    if top == OsStr::new(".git") || top == OsStr::new(".tenex") {
        return None;
    }

    Some(rel_path)
}

fn list_ignored_rel_paths(repo_root: &Path) -> Result<Vec<PathBuf>> {
    let output = super::git_command()
        .args([
            "ls-files",
            "-o",
            "-i",
            "--exclude-standard",
            "--directory",
            "-z",
        ])
        .current_dir(repo_root)
        .output()
        .with_context(|| format!("Failed to list ignored files from {}", repo_root.display()))?;

    if !output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("git ls-files failed (stdout: {stdout}, stderr: {stderr})");
    }

    Ok(output
        .stdout
        .split(|byte| *byte == b'\0')
        .filter_map(normalize_ignored_rel_path)
        .collect())
}

fn git_check_ignore_ignored_paths(
    repo_dir: &Path,
    rel_paths: &[PathBuf],
) -> Result<std::collections::HashSet<PathBuf>> {
    use std::io::Write;
    use std::process::Stdio;

    if rel_paths.is_empty() {
        return Ok(std::collections::HashSet::new());
    }

    let mut child = super::git_command()
        .args(["check-ignore", "-z", "--stdin"])
        .current_dir(repo_dir)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .context("Failed to spawn git check-ignore")?;

    {
        let mut stdin = child.stdin.take().context("Failed to open git stdin")?;
        for rel_path in rel_paths {
            let rel_path_bytes = git_path_bytes(rel_path);
            stdin.write_all(&rel_path_bytes).with_context(|| {
                format!(
                    "Failed to write path {} to git check-ignore stdin",
                    rel_path.display()
                )
            })?;
            stdin
                .write_all(b"\0")
                .context("Failed to write NUL delimiter to git check-ignore stdin")?;
        }
    }

    let output = child
        .wait_with_output()
        .context("Failed to read git check-ignore output")?;

    if !output.status.success() && output.status.code() != Some(1) {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("git check-ignore failed (stdout: {stdout}, stderr: {stderr})");
    }

    let mut fields = output
        .stdout
        .split(|byte| *byte == b'\0')
        .collect::<Vec<_>>();
    if fields.last().is_some_and(|value| value.is_empty()) {
        fields.pop();
    }

    Ok(fields
        .into_iter()
        .filter(|value| !value.is_empty())
        .map(git_path_from_bytes)
        .collect())
}

fn git_path_bytes(path: &Path) -> Vec<u8> {
    #[cfg(unix)]
    {
        use std::os::unix::ffi::OsStrExt;
        path.as_os_str().as_bytes().to_vec()
    }

    #[cfg(not(unix))]
    {
        path.to_string_lossy().into_owned().into_bytes()
    }
}

fn git_path_from_bytes(value: &[u8]) -> PathBuf {
    #[cfg(unix)]
    {
        use std::ffi::OsString;
        use std::os::unix::ffi::OsStringExt;
        PathBuf::from(OsString::from_vec(value.to_vec()))
    }

    #[cfg(not(unix))]
    {
        PathBuf::from(String::from_utf8_lossy(value).into_owned())
    }
}

fn symlink_path(src: &Path, dst: &Path) -> std::io::Result<()> {
    #[cfg(unix)]
    {
        std::os::unix::fs::symlink(src, dst)
    }

    #[cfg(windows)]
    {
        let metadata = fs::symlink_metadata(src)?;
        if metadata.file_type().is_dir() {
            std::os::windows::fs::symlink_dir(src, dst)
        } else {
            std::os::windows::fs::symlink_file(src, dst)
        }
    }
}

fn clone_symlink(src: &Path, dst: &Path) -> std::io::Result<()> {
    let link_target = fs::read_link(src)?;

    #[cfg(unix)]
    {
        std::os::unix::fs::symlink(&link_target, dst)
    }

    #[cfg(windows)]
    {
        let resolved_target = if link_target.is_absolute() {
            link_target.clone()
        } else {
            src.parent()
                .unwrap_or_else(|| Path::new("."))
                .join(&link_target)
        };
        if fs::metadata(&resolved_target)?.is_dir() {
            std::os::windows::fs::symlink_dir(&link_target, dst)
        } else {
            std::os::windows::fs::symlink_file(&link_target, dst)
        }
    }
}

/// Options controlling how a new worktree is materialized on disk.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CreateOptions {
    /// Whether ignored repo-root files should be linked into the new worktree.
    pub link_ignored_files: bool,
}

impl Default for CreateOptions {
    fn default() -> Self {
        Self {
            link_ignored_files: true,
        }
    }
}

impl CreateOptions {
    /// Return options for worktrees that should not inherit ignored repo-root file links.
    #[must_use]
    pub const fn without_ignored_file_links() -> Self {
        Self {
            link_ignored_files: false,
        }
    }
}

/// Manager for git worktree operations
pub struct Manager<'a> {
    repo: &'a Repository,
}

impl std::fmt::Debug for Manager<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Manager").finish_non_exhaustive()
    }
}

impl<'a> Manager<'a> {
    /// Create a new worktree manager for the given repository
    #[must_use]
    pub const fn new(repo: &'a Repository) -> Self {
        Self { repo }
    }

    /// Create a new worktree for a branch
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree cannot be created
    pub fn create(&self, path: &Path, branch: &str) -> Result<()> {
        self.create_with_options(path, branch, CreateOptions::default())
    }

    /// Create a new worktree for a branch with explicit materialization options.
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree cannot be created.
    pub fn create_with_options(
        &self,
        path: &Path,
        branch: &str,
        options: CreateOptions,
    ) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).with_context(|| {
                format!("Failed to create parent directory {}", parent.display())
            })?;
        }

        let branch_ref = self
            .repo
            .find_branch(branch, git2::BranchType::Local)
            .with_context(|| format!("Branch not found: {branch}"))?;

        let reference = branch_ref.into_reference();

        // Worktree name cannot contain slashes (it becomes a directory name in .git/worktrees/)
        let worktree_name = branch.replace('/', "-");

        let mut add_opts = git2::WorktreeAddOptions::new();
        add_opts.reference(Some(&reference));

        if let Err(err) = self.repo.worktree(&worktree_name, path, Some(&add_opts)) {
            if Self::should_force_worktree_add(path, &worktree_name, &err) {
                self.create_with_git_force(path, branch)
                    .with_context(|| format!("Failed to create worktree at {}", path.display()))?;
            } else {
                return Err(err)
                    .with_context(|| format!("Failed to create worktree at {}", path.display()));
            }
        }

        self.finish_worktree_create(path, options);

        Ok(())
    }

    fn should_force_worktree_add(path: &Path, worktree_name: &str, err: &git2::Error) -> bool {
        if err.class() != git2::ErrorClass::Worktree {
            return false;
        }
        if !err.message().contains("already checked out") {
            return false;
        }

        let Some(leaf) = path.file_name().and_then(|name| name.to_str()) else {
            return false;
        };

        leaf == worktree_name
    }

    fn create_with_git_force(&self, path: &Path, branch: &str) -> Result<()> {
        let repo_root = self
            .repo
            .workdir()
            .context("Repository has no working directory")?;

        let output = super::git_command()
            .args(["worktree", "add", "--force"])
            .arg(path)
            .arg(branch)
            .current_dir(repo_root)
            .stdin(Stdio::null())
            .output()
            .with_context(|| format!("Failed to run git worktree add for branch '{branch}'"))?;

        if !output.status.success() {
            let stdout = String::from_utf8_lossy(&output.stdout);
            let stderr = String::from_utf8_lossy(&output.stderr);
            bail!("git worktree add failed (stdout: {stdout}, stderr: {stderr})");
        }

        Ok(())
    }

    /// Create a worktree with a new branch from HEAD
    ///
    /// If the branch already exists (e.g., from a previous run), it will be deleted
    /// and recreated from HEAD.
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree or branch cannot be created
    pub fn create_with_new_branch(&self, path: &Path, branch: &str) -> Result<()> {
        self.create_with_new_branch_with_options(path, branch, CreateOptions::default())
    }

    /// Create a new worktree with a new branch from HEAD using explicit materialization options.
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree or branch cannot be created.
    pub fn create_with_new_branch_with_options(
        &self,
        path: &Path,
        branch: &str,
        options: CreateOptions,
    ) -> Result<()> {
        debug!(branch, ?path, "Creating worktree with new branch");

        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).with_context(|| {
                format!("Failed to create parent directory {}", parent.display())
            })?;
        }

        let head = self.repo.head().context("Failed to get HEAD")?;
        let commit = head.peel_to_commit().context("Failed to get HEAD commit")?;

        // Clean up any existing worktree/branch with this name (from a previous run)
        // Must remove worktree first, as the branch can't be deleted while it's
        // the HEAD of a linked worktree
        let worktree_name = branch.replace('/', "-");
        if let Ok(worktree) = self.repo.find_worktree(&worktree_name) {
            debug!(branch, "Removing existing worktree before recreation");
            let wt_path = worktree.path().to_path_buf();
            let _ = worktree.prune(Some(
                git2::WorktreePruneOptions::new()
                    .valid(true)
                    .working_tree(true),
            ));
            if wt_path.exists() {
                let _ = fs::remove_dir_all(&wt_path);
            }
        }

        self.cleanup_orphaned_worktree_admin_dir(&worktree_name)?;

        // Now delete the branch if it exists
        let branch_mgr = super::BranchManager::new(self.repo);
        if branch_mgr.exists(branch) {
            debug!(branch, "Deleting existing branch before recreation");
            let _ = branch_mgr.delete(branch);
        }

        let branch_ref = self
            .repo
            .branch(branch, &commit, false)
            .with_context(|| format!("Failed to create branch '{branch}'"))?;

        let reference = branch_ref.into_reference();

        self.repo
            .worktree(
                &worktree_name,
                path,
                Some(git2::WorktreeAddOptions::new().reference(Some(&reference))),
            )
            .with_context(|| format!("Failed to create worktree at {}", path.display()))?;

        self.finish_worktree_create(path, options);

        info!(branch, ?path, "Worktree created");
        Ok(())
    }

    fn finish_worktree_create(&self, path: &Path, options: CreateOptions) {
        if options.link_ignored_files
            && let Err(err) = self.symlink_ignored_files_into_worktree(path)
        {
            warn!(?path, error = %err, "Failed to symlink ignored files into worktree");
        }

        if let Err(err) = self.symlink_local_instruction_files_into_worktree(path) {
            warn!(?path, error = %err, "Failed to symlink local instruction files into worktree");
        }
    }

    fn symlink_local_instruction_files_into_worktree(&self, worktree_path: &Path) -> Result<()> {
        let Some(repo_root) = self.repo.workdir() else {
            return Ok(());
        };

        for file_name in LOCAL_INSTRUCTION_FILE_NAMES {
            Self::symlink_local_instruction_file(repo_root, worktree_path, file_name)?;
        }

        Ok(())
    }

    fn symlink_local_instruction_file(
        repo_root: &Path,
        worktree_path: &Path,
        file_name: &str,
    ) -> Result<()> {
        let src = repo_root.join(file_name);
        let Ok(src_meta) = fs::symlink_metadata(&src) else {
            return Ok(());
        };

        let dst = worktree_path.join(file_name);
        if fs::symlink_metadata(&dst).is_ok() {
            return Ok(());
        }

        if src_meta.file_type().is_symlink() {
            clone_symlink(&src, &dst).with_context(|| {
                format!(
                    "Failed to symlink local instruction file {} -> {}",
                    dst.display(),
                    src.display()
                )
            })?;
            return Ok(());
        }

        if !src_meta.is_file() {
            return Ok(());
        }

        symlink_path(&src, &dst).with_context(|| {
            format!(
                "Failed to symlink local instruction file {} -> {}",
                dst.display(),
                src.display()
            )
        })?;

        Ok(())
    }

    fn cleanup_orphaned_worktree_admin_dir(&self, worktree_name: &str) -> Result<()> {
        let admin_dir = self.repo.path().join("worktrees").join(worktree_name);
        if !admin_dir.exists() {
            return Ok(());
        }

        if !admin_dir.is_dir() {
            bail!(
                "Worktree admin path exists but is not a directory: {}",
                admin_dir.display()
            );
        }

        if is_empty_dir(&admin_dir)? {
            warn!(?admin_dir, "Removing orphaned worktree admin directory");
            remove_dir_all_with_retries(&admin_dir)?;
            return Ok(());
        }

        let gitdir_path = admin_dir.join("gitdir");
        if !gitdir_path.exists() {
            warn!(
                ?admin_dir,
                "Removing invalid worktree admin directory (missing gitdir)"
            );
            remove_dir_all_with_retries(&admin_dir)?;
            return Ok(());
        }

        let raw_gitdir = fs::read_to_string(&gitdir_path)
            .with_context(|| format!("Failed to read {}", gitdir_path.display()))?;
        let gitdir = raw_gitdir.trim();
        if gitdir.is_empty() {
            warn!(
                ?admin_dir,
                "Removing invalid worktree admin directory (empty gitdir)"
            );
            remove_dir_all_with_retries(&admin_dir)?;
            return Ok(());
        }

        let candidate = PathBuf::from(gitdir);
        let resolved = if candidate.is_absolute() {
            candidate
        } else {
            admin_dir.join(candidate)
        };

        if !resolved.exists() {
            warn!(
                ?admin_dir,
                gitdir = %resolved.display(),
                "Removing stale worktree admin directory"
            );
            remove_dir_all_with_retries(&admin_dir)?;
        }

        Ok(())
    }

    /// Remove a worktree and its associated branch
    ///
    /// Always attempts to delete the branch, even if the worktree is missing.
    /// This ensures cleanup works even if the worktree was manually removed.
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree exists but cannot be removed after retries.
    /// Does not return errors for missing worktrees or branches.
    pub fn remove(&self, name: &str) -> Result<()> {
        debug!(name, "Removing worktree and branch");

        // Worktree name has slashes replaced with dashes
        let worktree_name = name.replace('/', "-");

        // Try to remove the worktree with retries (may already be gone)
        if let Ok(worktree) = self.repo.find_worktree(&worktree_name) {
            let wt_path = worktree.path().to_path_buf();

            // Retry prune up to 3 times with increasing delays
            // This handles race conditions where processes are still terminating
            let mut prune_succeeded = false;
            for attempt in 0u64..3 {
                if attempt > 0 {
                    debug!(name, attempt, "Retrying worktree prune");
                    std::thread::sleep(std::time::Duration::from_millis(100 * attempt));
                }

                match worktree.prune(Some(
                    git2::WorktreePruneOptions::new()
                        .valid(true)
                        .working_tree(true),
                )) {
                    Ok(()) => {
                        prune_succeeded = true;
                        break;
                    }
                    Err(e) => {
                        debug!(name, error = %e, attempt, "Worktree prune failed");
                    }
                }
            }

            // Ensure libgit2 doesn't keep the worktree alive on platforms with strict file locking.
            drop(worktree);

            // Always try to remove the directory even if prune failed (may take time for processes
            // to release handles).
            if let Err(e) = remove_dir_all_with_retries(&wt_path) {
                warn!(name, path = ?wt_path, error = %e, "Failed to remove worktree directory");
                return Err(e);
            }

            // Verify the worktree is actually gone from git's perspective
            if !prune_succeeded {
                // Check if it's still registered with git
                if self.repo.find_worktree(&worktree_name).is_ok() {
                    warn!(name, "Worktree still exists in git after prune attempts");
                    return Err(anyhow::anyhow!(
                        "Failed to remove worktree '{name}' - it may still be in use"
                    ));
                }
            }

            debug!(name, "Worktree pruned");
        }

        // Always try to delete the branch (critical for cleanup)
        // Ignore errors - branch may already be deleted or checked out elsewhere
        let branch_mgr = super::BranchManager::new(self.repo);
        let _ = branch_mgr.delete(name);

        info!(name, "Worktree removed");
        Ok(())
    }

    /// Remove a worktree but keep its associated branch.
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree exists but cannot be removed after retries.
    /// Does not return errors for missing worktrees.
    pub fn remove_worktree_only(&self, name: &str) -> Result<()> {
        debug!(name, "Removing worktree (keeping branch)");

        // Worktree name has slashes replaced with dashes
        let worktree_name = name.replace('/', "-");

        // Try to remove the worktree with retries (may already be gone)
        if let Ok(worktree) = self.repo.find_worktree(&worktree_name) {
            let wt_path = worktree.path().to_path_buf();

            // Retry prune up to 3 times with increasing delays
            // This handles race conditions where processes are still terminating
            let mut prune_succeeded = false;
            for attempt in 0u64..3 {
                if attempt > 0 {
                    debug!(name, attempt, "Retrying worktree prune");
                    std::thread::sleep(std::time::Duration::from_millis(100 * attempt));
                }

                match worktree.prune(Some(
                    git2::WorktreePruneOptions::new()
                        .valid(true)
                        .working_tree(true),
                )) {
                    Ok(()) => {
                        prune_succeeded = true;
                        break;
                    }
                    Err(e) => {
                        debug!(name, error = %e, attempt, "Worktree prune failed");
                    }
                }
            }

            // Ensure libgit2 doesn't keep the worktree alive on platforms with strict file locking.
            drop(worktree);

            // Always try to remove the directory even if prune failed (may take time for processes
            // to release handles).
            if let Err(e) = remove_dir_all_with_retries(&wt_path) {
                warn!(name, path = ?wt_path, error = %e, "Failed to remove worktree directory");
                return Err(e);
            }

            // Verify the worktree is actually gone from git's perspective
            if !prune_succeeded && self.repo.find_worktree(&worktree_name).is_ok() {
                warn!(name, "Worktree still exists in git after prune attempts");
                return Err(anyhow::anyhow!(
                    "Failed to remove worktree '{name}' - it may still be in use"
                ));
            }

            debug!(name, "Worktree pruned");
        }

        info!(name, "Worktree removed (branch preserved)");
        Ok(())
    }

    /// List all worktrees
    ///
    /// # Errors
    ///
    /// Returns an error if worktrees cannot be listed
    pub fn list(&self) -> Result<Vec<Info>> {
        let worktrees = self.repo.worktrees().context("Failed to list worktrees")?;

        let mut infos = Vec::new();
        for name in worktrees.iter().flatten() {
            if let Ok(wt) = self.repo.find_worktree(name) {
                let is_locked = matches!(wt.is_locked(), Ok(git2::WorktreeLockStatus::Locked(_)));
                infos.push(Info {
                    name: name.to_string(),
                    path: wt.path().to_path_buf(),
                    is_locked,
                });
            }
        }

        Ok(infos)
    }

    /// Check if a worktree exists
    #[must_use]
    pub fn exists(&self, name: &str) -> bool {
        let worktree_name = name.replace('/', "-");
        self.repo.find_worktree(&worktree_name).is_ok()
    }

    /// Returns the filesystem path for a worktree, if present.
    #[must_use]
    pub fn worktree_path(&self, name: &str) -> Option<PathBuf> {
        let worktree_name = name.replace('/', "-");
        self.repo
            .find_worktree(&worktree_name)
            .ok()
            .map(|wt| wt.path().to_path_buf())
    }

    /// Lock a worktree to prevent it from being pruned
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree cannot be locked
    pub fn lock(&self, name: &str, reason: Option<&str>) -> Result<()> {
        let worktree_name = name.replace('/', "-");
        let worktree = self
            .repo
            .find_worktree(&worktree_name)
            .with_context(|| format!("Worktree not found: {name}"))?;

        worktree
            .lock(reason)
            .with_context(|| format!("Failed to lock worktree '{name}'"))?;

        Ok(())
    }

    /// Unlock a worktree
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree cannot be unlocked
    pub fn unlock(&self, name: &str) -> Result<()> {
        let worktree_name = name.replace('/', "-");
        let worktree = self
            .repo
            .find_worktree(&worktree_name)
            .with_context(|| format!("Worktree not found: {name}"))?;

        let is_locked = matches!(
            worktree.is_locked(),
            Ok(git2::WorktreeLockStatus::Locked(_))
        );
        if !is_locked {
            bail!("Worktree '{name}' is not locked");
        }

        worktree
            .unlock()
            .with_context(|| format!("Failed to unlock worktree '{name}'"))?;

        Ok(())
    }

    /// Validate a worktree (check if it's valid)
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree is invalid
    pub fn validate(&self, name: &str) -> Result<()> {
        let worktree_name = name.replace('/', "-");
        let worktree = self
            .repo
            .find_worktree(&worktree_name)
            .with_context(|| format!("Worktree not found: {name}"))?;

        worktree
            .validate()
            .with_context(|| format!("Worktree '{name}' is invalid"))?;

        Ok(())
    }

    /// Get the HEAD commit information for the main repository
    ///
    /// Returns (`branch_name`, `short_commit_hash`)
    ///
    /// # Errors
    ///
    /// Returns an error if HEAD cannot be read
    pub fn head_info(&self) -> Result<(String, String)> {
        let head = self.repo.head().context("Failed to get HEAD")?;
        let commit = head.peel_to_commit().context("Failed to get HEAD commit")?;

        let branch_name = if head.is_branch() {
            head.shorthand()
                .map_or_else(|| "HEAD".to_string(), String::from)
        } else {
            "HEAD (detached)".to_string()
        };

        let short_hash = commit.id().to_string()[..7].to_string();

        Ok((branch_name, short_hash))
    }

    /// Get the HEAD commit information for an existing worktree
    ///
    /// Returns (`branch_name`, `short_commit_hash`) if the worktree exists and has a valid HEAD
    ///
    /// # Errors
    ///
    /// Returns an error if the worktree or its HEAD cannot be read
    pub fn worktree_head_info(&self, name: &str) -> Result<(String, String)> {
        let worktree_name = name.replace('/', "-");
        let worktree = self
            .repo
            .find_worktree(&worktree_name)
            .with_context(|| format!("Worktree not found: {name}"))?;

        // Open the worktree as a repository to get its HEAD
        let wt_repo = git2::Repository::open(worktree.path()).with_context(|| {
            format!(
                "Failed to open worktree repository at {}",
                worktree.path().display()
            )
        })?;

        let head = wt_repo.head().context("Failed to get worktree HEAD")?;
        let commit = head
            .peel_to_commit()
            .context("Failed to get worktree HEAD commit")?;

        let branch_name = if head.is_branch() {
            head.shorthand()
                .map_or_else(|| "HEAD".to_string(), String::from)
        } else {
            "HEAD (detached)".to_string()
        };

        let short_hash = commit.id().to_string()[..7].to_string();

        Ok((branch_name, short_hash))
    }

    fn symlink_ignored_files_into_worktree(&self, worktree_path: &Path) -> Result<()> {
        let Some(repo_root) = self.repo.workdir() else {
            return Ok(());
        };

        let rel_paths = list_ignored_rel_paths(repo_root)?;
        let ignored_as_file_in_worktree =
            git_check_ignore_ignored_paths(worktree_path, &rel_paths)?;

        for rel_path in rel_paths {
            Self::symlink_ignored_path(
                repo_root,
                worktree_path,
                &rel_path,
                &ignored_as_file_in_worktree,
            )?;
        }

        Ok(())
    }

    fn symlink_ignored_path(
        repo_root: &Path,
        worktree_path: &Path,
        rel_path: &Path,
        ignored_as_file_in_worktree: &std::collections::HashSet<PathBuf>,
    ) -> Result<()> {
        if !ignored_as_file_in_worktree.contains(rel_path) {
            debug!(
                "Skipping ignored path {} because it is not ignored as a file in the worktree",
                rel_path.display()
            );
            return Ok(());
        }

        let src = repo_root.join(rel_path);
        let Ok(src_meta) = fs::symlink_metadata(&src) else {
            return Ok(());
        };

        if src_meta.file_type().is_symlink() {
            return Ok(());
        }

        if worktree_path.starts_with(&src) {
            return Ok(());
        }

        let dst = worktree_path.join(rel_path);
        if fs::symlink_metadata(&dst).is_ok() {
            return Ok(());
        }

        let Some(parent) = dst.parent() else {
            return Ok(());
        };

        let parent_is_symlink = fs::symlink_metadata(parent)
            .map(|m| m.file_type().is_symlink())
            .unwrap_or(false);
        if parent_is_symlink {
            return Ok(());
        }

        fs::create_dir_all(parent).with_context(|| {
            format!(
                "Failed to create parent directory {} for ignored file link",
                parent.display()
            )
        })?;

        symlink_path(&src, &dst).with_context(|| {
            format!(
                "Failed to symlink ignored path {} -> {}",
                dst.display(),
                src.display()
            )
        })?;

        Ok(())
    }
}

/// Information about a worktree
#[derive(Debug, Clone)]
pub struct Info {
    /// Name of the worktree (usually branch name)
    pub name: String,
    /// Path to the worktree directory
    pub path: PathBuf,
    /// Whether the worktree is locked
    pub is_locked: bool,
}

#[cfg(test)]
mod tests {
    use super::*;
    use git2::Signature;
    use tempfile::TempDir;

    fn init_test_repo_with_commit() -> Result<(TempDir, Repository), Box<dyn std::error::Error>> {
        let temp_dir = TempDir::new()?;
        let repo = Repository::init(temp_dir.path())?;

        let sig = Signature::now("Test", "test@test.com")?;
        let file_path = temp_dir.path().join("README.md");
        fs::write(&file_path, "# Test")?;

        let mut index = repo.index()?;
        index.add_path(Path::new("README.md"))?;
        index.write()?;

        let tree_id = index.write_tree()?;

        {
            let tree = repo.find_tree(tree_id)?;
            repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])?;
        }

        Ok((temp_dir, repo))
    }

    #[test]
    fn test_create_with_new_branch() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("feature-test");
        manager.create_with_new_branch(&wt_path, "feature-test")?;

        assert!(wt_path.exists());
        assert!(manager.exists("feature-test"));
        Ok(())
    }

    #[test]
    fn test_create_with_new_branch_removes_orphaned_admin_dir()
    -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let admin_dir = repo.path().join("worktrees").join("agent-asdf");
        fs::create_dir_all(&admin_dir)?;
        assert!(is_empty_dir(&admin_dir)?);

        let wt_path = temp_dir.path().join("worktrees").join("asdf");
        manager.create_with_new_branch(&wt_path, "agent/asdf")?;

        assert!(wt_path.exists());
        assert!(manager.exists("agent/asdf"));
        assert!(admin_dir.join("gitdir").exists());
        Ok(())
    }

    #[test]
    fn test_create_with_new_branch_symlinks_ignored_files() -> Result<(), Box<dyn std::error::Error>>
    {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        fs::write(
            temp_dir.path().join(".gitignore"),
            "ignored.txt\nignored_dir/\n",
        )?;

        let sig = Signature::now("Test", "test@test.com")?;
        let parent_commit = repo.head()?.peel_to_commit()?;

        let mut index = repo.index()?;
        index.add_path(Path::new(".gitignore"))?;
        index.write()?;

        let tree_id = index.write_tree()?;
        let tree = repo.find_tree(tree_id)?;
        repo.commit(
            Some("HEAD"),
            &sig,
            &sig,
            "Add gitignore",
            &tree,
            &[&parent_commit],
        )?;

        let ignored_file = temp_dir.path().join("ignored.txt");
        fs::write(&ignored_file, "payload")?;

        let ignored_dir = temp_dir.path().join("ignored_dir");
        fs::create_dir_all(&ignored_dir)?;
        fs::write(ignored_dir.join("nested.txt"), "nested")?;

        let wt_path = temp_dir.path().join("worktrees").join("feature-symlinks");
        manager.create_with_new_branch(&wt_path, "feature-symlinks-test")?;

        let linked_file = wt_path.join("ignored.txt");
        let linked_meta = fs::symlink_metadata(&linked_file)?;
        assert!(linked_meta.file_type().is_symlink());
        assert_eq!(
            fs::canonicalize(&linked_file)?,
            fs::canonicalize(&ignored_file)?
        );

        assert!(fs::symlink_metadata(wt_path.join("ignored_dir")).is_err());

        Ok(())
    }

    #[test]
    fn test_create_with_new_branch_can_skip_ignored_file_symlinks()
    -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        fs::write(temp_dir.path().join(".gitignore"), "ignored.txt\n")?;

        let sig = Signature::now("Test", "test@test.com")?;
        let parent_commit = repo.head()?.peel_to_commit()?;

        let mut index = repo.index()?;
        index.add_path(Path::new(".gitignore"))?;
        index.write()?;

        let tree_id = index.write_tree()?;
        let tree = repo.find_tree(tree_id)?;
        repo.commit(
            Some("HEAD"),
            &sig,
            &sig,
            "Add gitignore",
            &tree,
            &[&parent_commit],
        )?;

        fs::write(temp_dir.path().join("ignored.txt"), "payload")?;
        let agents_path = temp_dir.path().join("AGENTS.md");
        fs::write(&agents_path, "# local instructions\n")?;
        symlink_path(Path::new("AGENTS.md"), &temp_dir.path().join("CLAUDE.md"))?;

        let wt_path = temp_dir
            .path()
            .join("worktrees")
            .join("feature-no-symlinks");
        manager.create_with_new_branch_with_options(
            &wt_path,
            "feature-no-symlinks-test",
            CreateOptions::without_ignored_file_links(),
        )?;

        assert!(fs::symlink_metadata(wt_path.join("ignored.txt")).is_err());
        let linked_agents = wt_path.join("AGENTS.md");
        assert!(
            fs::symlink_metadata(&linked_agents)?
                .file_type()
                .is_symlink()
        );
        assert_eq!(
            fs::canonicalize(&linked_agents)?,
            fs::canonicalize(&agents_path)?
        );

        let linked_claude = wt_path.join("CLAUDE.md");
        assert!(
            fs::symlink_metadata(&linked_claude)?
                .file_type()
                .is_symlink()
        );
        assert_eq!(fs::read_link(&linked_claude)?, PathBuf::from("AGENTS.md"));
        assert_eq!(
            fs::canonicalize(&linked_claude)?,
            fs::canonicalize(&agents_path)?
        );
        Ok(())
    }

    #[test]
    fn test_symlink_local_instruction_file_skips_existing_dst()
    -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = TempDir::new()?;
        let repo_root = temp_dir.path().join("repo-root");
        let worktree_root = temp_dir.path().join("worktree-root");
        fs::create_dir_all(&repo_root)?;
        fs::create_dir_all(&worktree_root)?;

        let src = repo_root.join("AGENTS.md");
        fs::write(&src, "src")?;

        let dst = worktree_root.join("AGENTS.md");
        fs::write(&dst, "dst")?;

        Manager::symlink_local_instruction_file(&repo_root, &worktree_root, "AGENTS.md")?;

        assert_eq!(fs::read_to_string(&dst)?, "dst");
        Ok(())
    }

    #[test]
    fn test_manager_debug_impl() -> Result<(), Box<dyn std::error::Error>> {
        let (_temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        assert_eq!(format!("{manager:?}"), "Manager { .. }");
        Ok(())
    }

    #[test]
    fn test_create_with_new_branch_skips_dir_only_ignored_directory_symlinks()
    -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        fs::write(temp_dir.path().join(".gitignore"), ".ruff_cache/\n")?;

        let sig = Signature::now("Test", "test@test.com")?;
        let parent_commit = repo.head()?.peel_to_commit()?;

        let mut index = repo.index()?;
        index.add_path(Path::new(".gitignore"))?;
        index.write()?;

        let tree_id = index.write_tree()?;
        let tree = repo.find_tree(tree_id)?;
        repo.commit(
            Some("HEAD"),
            &sig,
            &sig,
            "Add gitignore",
            &tree,
            &[&parent_commit],
        )?;

        let cache_dir = temp_dir.path().join(".ruff_cache");
        fs::create_dir_all(&cache_dir)?;
        fs::write(cache_dir.join("marker.txt"), "payload")?;

        let wt_path = temp_dir
            .path()
            .join("worktrees")
            .join("feature-ignored-dir");
        manager.create_with_new_branch(&wt_path, "feature-ignored-dir-test")?;

        assert!(fs::symlink_metadata(wt_path.join(".ruff_cache")).is_err());

        Ok(())
    }

    #[test]
    fn test_create_with_new_branch_symlinks_ignored_dir_when_ignored_as_file()
    -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        fs::write(temp_dir.path().join(".gitignore"), "ignored_dir\n")?;

        let sig = Signature::now("Test", "test@test.com")?;
        let parent_commit = repo.head()?.peel_to_commit()?;

        let mut index = repo.index()?;
        index.add_path(Path::new(".gitignore"))?;
        index.write()?;

        let tree_id = index.write_tree()?;
        let tree = repo.find_tree(tree_id)?;
        repo.commit(
            Some("HEAD"),
            &sig,
            &sig,
            "Add gitignore",
            &tree,
            &[&parent_commit],
        )?;

        let ignored_dir = temp_dir.path().join("ignored_dir");
        fs::create_dir_all(&ignored_dir)?;
        fs::write(ignored_dir.join("nested.txt"), "nested")?;

        let wt_path = temp_dir
            .path()
            .join("worktrees")
            .join("feature-ignored-dir-no-slash");
        manager.create_with_new_branch(&wt_path, "feature-ignored-dir-no-slash-test")?;

        let linked_dir = wt_path.join("ignored_dir");
        let linked_dir_meta = fs::symlink_metadata(&linked_dir)?;
        assert!(linked_dir_meta.file_type().is_symlink());
        assert_eq!(
            fs::canonicalize(&linked_dir)?,
            fs::canonicalize(&ignored_dir)?
        );

        assert_eq!(fs::read_to_string(linked_dir.join("nested.txt"))?, "nested");

        let status = super::super::git_command()
            .args(["status", "--porcelain"])
            .current_dir(&wt_path)
            .output()?;
        assert!(status.status.success());

        let stdout = String::from_utf8_lossy(&status.stdout);
        assert!(
            !stdout.contains("ignored_dir"),
            "Expected ignored_dir to be ignored; git status output:\n{stdout}"
        );

        Ok(())
    }

    #[test]
    fn test_create_with_new_branch_skips_ignored_symlink_sources()
    -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        fs::write(temp_dir.path().join(".gitignore"), "ignored-link\n")?;

        let sig = Signature::now("Test", "test@test.com")?;
        let parent_commit = repo.head()?.peel_to_commit()?;

        let mut index = repo.index()?;
        index.add_path(Path::new(".gitignore"))?;
        index.write()?;

        let tree_id = index.write_tree()?;
        let tree = repo.find_tree(tree_id)?;
        repo.commit(
            Some("HEAD"),
            &sig,
            &sig,
            "Add gitignore",
            &tree,
            &[&parent_commit],
        )?;

        let real = temp_dir.path().join("real.txt");
        fs::write(&real, "payload")?;

        let link = temp_dir.path().join("ignored-link");
        symlink_path(&real, &link)?;

        let wt_path = temp_dir
            .path()
            .join("worktrees")
            .join("feature-symlink-source");
        manager.create_with_new_branch(&wt_path, "feature-symlink-source-test")?;

        assert!(fs::symlink_metadata(wt_path.join("ignored-link")).is_err());
        Ok(())
    }

    #[test]
    fn test_create_with_new_branch_skips_ignored_worktree_ancestor()
    -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        fs::write(temp_dir.path().join(".gitignore"), "worktrees/\n")?;

        let wt_path = temp_dir.path().join("worktrees").join("feature-loop");
        manager.create_with_new_branch(&wt_path, "feature-loop-test")?;

        assert!(fs::symlink_metadata(wt_path.join("worktrees")).is_err());
        Ok(())
    }

    #[test]
    fn test_create_existing_branch() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;

        let head = repo.head()?;
        let commit = head.peel_to_commit()?;
        repo.branch("existing-branch", &commit, false)?;

        let manager = Manager::new(&repo);
        let wt_path = temp_dir.path().join("worktrees").join("existing");
        manager.create(&wt_path, "existing-branch")?;

        assert!(wt_path.exists());
        Ok(())
    }

    #[test]
    fn test_create_checked_out_branch_uses_git_force() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let head = repo.head()?;
        let branch = head
            .shorthand()
            .ok_or_else(|| std::io::Error::other("Expected HEAD to be a branch"))?
            .to_string();

        let wt_path = temp_dir
            .path()
            .join("worktrees")
            .join(branch.replace('/', "-"));
        manager.create(&wt_path, &branch)?;

        assert!(wt_path.exists());
        assert!(manager.exists(&branch));
        Ok(())
    }

    #[test]
    fn test_create_nonexistent_branch() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("test");
        let result = manager.create(&wt_path, "nonexistent");
        assert!(result.is_err());
        Ok(())
    }

    #[test]
    fn test_remove_worktree() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("feature-remove");
        manager.create_with_new_branch(&wt_path, "feature-remove-test")?;
        assert!(manager.exists("feature-remove-test"));

        manager.remove("feature-remove-test")?;
        assert!(!manager.exists("feature-remove-test"));
        assert!(!wt_path.exists());
        Ok(())
    }

    #[test]
    fn test_remove_nonexistent() -> Result<(), Box<dyn std::error::Error>> {
        // Removing a non-existent worktree/branch should succeed (idempotent cleanup)
        let (_temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let result = manager.remove("nonexistent");
        assert!(result.is_ok());
        Ok(())
    }

    #[test]
    fn test_remove_with_missing_worktree_but_existing_branch()
    -> Result<(), Box<dyn std::error::Error>> {
        // When worktree is manually removed but branch exists, cleanup should still delete branch
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("orphan-branch");
        manager.create_with_new_branch(&wt_path, "orphan-branch-test")?;

        // Verify branch exists
        assert!(
            repo.find_branch("orphan-branch-test", git2::BranchType::Local)
                .is_ok()
        );

        // Manually remove the worktree directory (simulating manual cleanup)
        fs::remove_dir_all(&wt_path)?;

        // Prune the worktree reference so git doesn't track it
        let worktree_name = "orphan-branch-test";
        if let Ok(wt) = repo.find_worktree(worktree_name) {
            let _ = wt.prune(Some(
                git2::WorktreePruneOptions::new()
                    .valid(true)
                    .working_tree(true),
            ));
        }

        // Now remove should still clean up the branch
        manager.remove("orphan-branch-test")?;

        // Branch should be deleted
        assert!(
            repo.find_branch("orphan-branch-test", git2::BranchType::Local)
                .is_err()
        );
        Ok(())
    }

    #[test]
    fn test_remove_dir_all_with_retries_noop_when_missing() -> Result<(), Box<dyn std::error::Error>>
    {
        let temp_dir = TempDir::new()?;
        let missing = temp_dir.path().join("missing-dir");

        remove_dir_all_with_retries(&missing)?;

        assert!(!missing.exists());
        Ok(())
    }

    #[test]
    fn test_remove_dir_all_with_retries_removes_existing_dir()
    -> Result<(), Box<dyn std::error::Error>> {
        let temp_dir = TempDir::new()?;
        let target = temp_dir.path().join("to-remove");
        let nested = target.join("nested");
        fs::create_dir_all(&nested)?;
        fs::write(nested.join("file.txt"), "payload")?;

        remove_dir_all_with_retries(&target)?;

        assert!(!target.exists());
        Ok(())
    }

    #[test]
    fn test_list_worktrees() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("feature-list");
        manager.create_with_new_branch(&wt_path, "feature-list-test")?;

        let worktrees = manager.list()?;
        assert!(worktrees.iter().any(|wt| wt.name == "feature-list-test"));
        Ok(())
    }

    #[test]
    fn test_exists() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        assert!(!manager.exists("nonexistent"));

        let wt_path = temp_dir.path().join("worktrees").join("feature-exists");
        manager.create_with_new_branch(&wt_path, "feature-exists-test")?;

        assert!(manager.exists("feature-exists-test"));
        Ok(())
    }

    #[test]
    fn test_lock_and_unlock() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("feature-lock");
        manager.create_with_new_branch(&wt_path, "feature-lock-test")?;

        manager.lock("feature-lock-test", Some("Testing lock"))?;

        let worktrees = manager.list()?;
        let locked_wt = worktrees
            .iter()
            .find(|wt| wt.name == "feature-lock-test")
            .ok_or("Expected worktree")?;
        assert!(locked_wt.is_locked);

        manager.unlock("feature-lock-test")?;

        let worktrees = manager.list()?;
        let unlocked_wt = worktrees
            .iter()
            .find(|wt| wt.name == "feature-lock-test")
            .ok_or("Expected worktree")?;
        assert!(!unlocked_wt.is_locked);
        Ok(())
    }

    #[test]
    fn test_unlock_not_locked() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("feature-unlock");
        manager.create_with_new_branch(&wt_path, "feature-unlock-test")?;

        let result = manager.unlock("feature-unlock-test");
        assert!(result.is_err());
        Ok(())
    }

    #[test]
    fn test_validate() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("feature-validate");
        manager.create_with_new_branch(&wt_path, "feature-validate-test")?;

        manager.validate("feature-validate-test")?;
        Ok(())
    }

    #[test]
    fn test_branch_name_with_slashes() -> Result<(), Box<dyn std::error::Error>> {
        // Integration test: branch names with slashes (like "tenex/feature-name")
        // should work correctly. The worktree name internally replaces slashes with dashes.
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        // Use a branch name with a slash (like tenex generates)
        let branch_name = "tenex/my-feature";
        let wt_path = temp_dir
            .path()
            .join("worktrees")
            .join("tenex")
            .join("my-feature");

        // Create worktree with slashed branch name
        manager.create_with_new_branch(&wt_path, branch_name)?;

        // Verify worktree directory exists
        assert!(wt_path.exists());

        // Verify worktree can be found using original branch name
        assert!(manager.exists(branch_name));

        // Verify the worktree is a valid git worktree (has .git file)
        assert!(wt_path.join(".git").exists());

        // Verify the branch was created in the repository
        assert!(
            repo.find_branch(branch_name, git2::BranchType::Local)
                .is_ok()
        );

        // Verify we can validate the worktree using the branch name
        manager.validate(branch_name)?;

        // Verify we can remove the worktree using the branch name
        manager.remove(branch_name)?;
        assert!(!manager.exists(branch_name));
        assert!(!wt_path.exists());
        Ok(())
    }

    #[test]
    fn test_worktree_info() {
        let info = Info {
            name: "test".to_string(),
            path: PathBuf::from("/tmp/test"),
            is_locked: false,
        };

        assert_eq!(info.name, "test");
        assert_eq!(info.path, PathBuf::from("/tmp/test"));
        assert!(!info.is_locked);
    }

    #[test]
    fn test_create_with_new_branch_cleanup_existing() -> Result<(), Box<dyn std::error::Error>> {
        // Test that create_with_new_branch cleans up existing worktree/branch
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("feature-recreate");

        // Create initial worktree
        manager.create_with_new_branch(&wt_path, "feature-recreate-test")?;
        assert!(wt_path.exists());
        assert!(manager.exists("feature-recreate-test"));

        // Create a file in the worktree to verify it's a new worktree after recreation
        let marker_file = wt_path.join("marker.txt");
        fs::write(&marker_file, "original")?;
        assert!(marker_file.exists());

        // Remove the worktree first (simulate user cleanup without branch deletion)
        manager.remove("feature-recreate-test")?;

        // Re-create with the same name - should clean up existing branch and succeed
        manager.create_with_new_branch(&wt_path, "feature-recreate-test")?;

        assert!(wt_path.exists());
        assert!(manager.exists("feature-recreate-test"));
        // The marker file should not exist (fresh worktree)
        assert!(!marker_file.exists());

        Ok(())
    }

    #[test]
    fn test_create_with_new_branch_recreate_with_existing_worktree()
    -> Result<(), Box<dyn std::error::Error>> {
        // Test that create_with_new_branch cleans up when worktree still exists
        // (exercises the cleanup path at lines 85-94)
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let wt_path = temp_dir.path().join("worktrees").join("feature-reuse");

        // Create initial worktree
        manager.create_with_new_branch(&wt_path, "feature-reuse-test")?;
        assert!(wt_path.exists());
        assert!(manager.exists("feature-reuse-test"));

        // Create a marker file
        let marker_file = wt_path.join("marker.txt");
        fs::write(&marker_file, "original")?;

        // DO NOT remove the worktree - call create_with_new_branch again
        // This should prune the existing worktree and recreate it
        manager.create_with_new_branch(&wt_path, "feature-reuse-test")?;

        assert!(wt_path.exists());
        assert!(manager.exists("feature-reuse-test"));
        // The marker file should not exist (fresh worktree)
        assert!(!marker_file.exists());

        Ok(())
    }

    #[test]
    fn test_head_info() -> Result<(), Box<dyn std::error::Error>> {
        let (_temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let (branch, commit) = manager.head_info()?;

        // Should be on master/main branch
        assert!(branch == "master" || branch == "main");
        // Commit hash should be 7 characters
        assert_eq!(commit.len(), 7);
        Ok(())
    }

    #[test]
    fn test_worktree_head_info() -> Result<(), Box<dyn std::error::Error>> {
        let (temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        // Create a worktree first
        let wt_path = temp_dir.path().join("worktrees").join("feature-info");
        manager.create_with_new_branch(&wt_path, "feature-info-test")?;

        let (branch, commit) = manager.worktree_head_info("feature-info-test")?;

        // Should be on the feature branch
        assert_eq!(branch, "feature-info-test");
        // Commit hash should be 7 characters
        assert_eq!(commit.len(), 7);
        Ok(())
    }

    #[test]
    fn test_worktree_head_info_not_found() -> Result<(), Box<dyn std::error::Error>> {
        let (_temp_dir, repo) = init_test_repo_with_commit()?;
        let manager = Manager::new(&repo);

        let result = manager.worktree_head_info("nonexistent-worktree");
        assert!(result.is_err());
        Ok(())
    }
}
