//! Native git worktree implementation.

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use crate::config::{PullStrategy, WorktreeConfig};
use crate::error::WorktreeError;

use super::{GitWorktreeStatus, RemoteUrlParts, WorktreeInfo, WorktreeManager, parse_remote_url};

/// Expand tilde in path string
fn expand_tilde(path: &str) -> PathBuf {
    if let Some(stripped) = path.strip_prefix("~/")
        && let Some(home) = dirs::home_dir()
    {
        return home.join(stripped);
    }
    PathBuf::from(path)
}

/// Native worktree manager using git CLI
#[derive(Debug)]
pub struct NativeWorktreeManager {
    repo_root: PathBuf,
    repo_name: String,
    base_path: PathBuf,
    config: WorktreeConfig,
    /// Parsed remote URL for hierarchical path generation
    remote_parts: Option<RemoteUrlParts>,
}

impl NativeWorktreeManager {
    /// Create a new manager at the specified repository root
    ///
    /// # Errors
    /// - `NotGitRepo` if the path is not a git repository
    /// - `Io` if git command fails to execute
    pub fn new(repo_root: PathBuf, config: WorktreeConfig) -> Result<Self, WorktreeError> {
        let output = Command::new("git")
            .args(["rev-parse", "--git-dir"])
            .current_dir(&repo_root)
            .output()?;

        if !output.status.success() {
            return Err(WorktreeError::NotGitRepo(repo_root));
        }

        // Fetch and parse remote URL for hierarchical paths
        let remote_parts = Self::fetch_remote_url(&repo_root).ok().flatten();

        // Derive repo_name: prefer remote_parts.repo, fallback to directory name
        let repo_name = remote_parts.as_ref().map_or_else(
            || {
                repo_root
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("unknown")
                    .to_string()
            },
            |p| p.repo.clone(),
        );

        let base_path = expand_tilde(&config.base_path);

        Ok(Self {
            repo_root,
            repo_name,
            base_path,
            config,
            remote_parts,
        })
    }

    /// Fetch the remote origin URL from the repository
    fn fetch_remote_url(repo_root: &Path) -> Result<Option<RemoteUrlParts>, std::io::Error> {
        let output = Command::new("git")
            .args(["remote", "get-url", "origin"])
            .current_dir(repo_root)
            .output()?;

        if !output.status.success() {
            // No remote configured or other error
            return Ok(None);
        }

        let url = String::from_utf8_lossy(&output.stdout);
        Ok(parse_remote_url(url.trim()))
    }

    /// Discover the main repository root from the current directory
    ///
    /// Works correctly even when called from inside a worktree,
    /// by using `git-common-dir` to find the shared `.git` directory.
    ///
    /// # Errors
    /// - `NotGitRepo` if not inside a git repository
    /// - `InvalidPath` if cannot determine main repo root
    /// - `Io` if git command fails
    pub fn discover(config: WorktreeConfig) -> Result<Self, WorktreeError> {
        // Use git-common-dir to find shared .git directory
        // This always points to main repo's .git, even from worktree
        let output = Command::new("git")
            .args(["rev-parse", "--path-format=absolute", "--git-common-dir"])
            .output()?;

        if !output.status.success() {
            let cwd = std::env::current_dir()?;
            return Err(WorktreeError::NotGitRepo(cwd));
        }

        // git-common-dir → /path/to/main-repo/.git
        // parent()       → /path/to/main-repo
        let git_common_dir = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
        let main_repo_root = git_common_dir
            .parent()
            .ok_or_else(|| WorktreeError::InvalidPath(git_common_dir.clone()))?
            .to_path_buf();

        Self::new(main_repo_root, config)
    }

    /// Get the repository name (derived from `repo_root`)
    #[must_use]
    pub fn repo_name(&self) -> &str {
        &self.repo_name
    }

    /// Get the base path for worktrees
    #[must_use]
    pub fn base_path(&self) -> &PathBuf {
        &self.base_path
    }
}

/// Parse git status --porcelain output to determine if repo is dirty
fn parse_git_status_dirty(output: &str) -> bool {
    !output.trim().is_empty()
}

/// Parse git rev-parse --abbrev-ref HEAD output to detect detached HEAD
fn parse_git_detached(output: &str) -> bool {
    output.trim() == "HEAD"
}

/// Parse git rev-list --count output to u32
fn parse_rev_list_count(output: &str) -> u32 {
    output.trim().parse().unwrap_or(0)
}

impl WorktreeManager for NativeWorktreeManager {
    fn create_worktree(
        &self,
        branch_name: Option<&str>,
    ) -> Result<(PathBuf, String), WorktreeError> {
        // Generate unique ID for auto-generated branch names
        let dir_id = uuid::Uuid::new_v4().to_string();
        let branch = branch_name.map_or_else(
            || format!("{}{}", self.config.branch_prefix, dir_id),
            String::from,
        );

        // Generate worktree path:
        // - With remote: {base_path}/{domain}/{user}/{repo}/{branch}
        // - Without remote: {base_path}/{repo_name}/{branch}
        let worktree_path = if let Some(ref parts) = self.remote_parts {
            self.base_path
                .join(&parts.domain)
                .join(&parts.user)
                .join(&parts.repo)
                .join(&branch)
        } else {
            self.base_path.join(&self.repo_name).join(&branch)
        };

        // Ensure parent directory exists
        let parent = worktree_path
            .parent()
            .ok_or_else(|| WorktreeError::InvalidPath(worktree_path.clone()))?;
        fs::create_dir_all(parent)?;

        // Check if worktree path already exists
        if worktree_path.exists() {
            return Err(WorktreeError::WorktreeExists {
                path: worktree_path,
            });
        }

        // git worktree add -b <branch> <path>
        let output = Command::new("git")
            .current_dir(&self.repo_root)
            .args(["worktree", "add", "-b", &branch])
            .arg(&worktree_path)
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            if stderr.contains("already exists") {
                return Err(WorktreeError::BranchExists(branch));
            }
            return Err(WorktreeError::GitFailed(stderr.trim().to_string()));
        }

        Ok((worktree_path, branch))
    }

    fn remove_worktree(&self, path: &Path) -> Result<(), WorktreeError> {
        // Check if worktree exists
        if !path.exists() {
            return Err(WorktreeError::WorktreeNotFound {
                path: path.to_path_buf(),
            });
        }

        // git worktree remove --force <path>
        let output = Command::new("git")
            .current_dir(&self.repo_root)
            .args(["worktree", "remove", "--force"])
            .arg(path)
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitFailed(stderr.trim().to_string()));
        }

        Ok(())
    }

    fn list_worktrees(&self) -> Result<Vec<WorktreeInfo>, WorktreeError> {
        let output = Command::new("git")
            .current_dir(&self.repo_root)
            .args(["worktree", "list", "--porcelain"])
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitFailed(stderr.trim().to_string()));
        }

        let stdout = String::from_utf8_lossy(&output.stdout);

        let mut worktrees = Vec::new();
        let mut current_path: Option<PathBuf> = None;
        let mut current_branch: Option<String> = None;

        let push_worktree =
            |worktrees: &mut Vec<WorktreeInfo>, path: PathBuf, branch: Option<String>| {
                let detached = branch.is_none();
                worktrees.push(WorktreeInfo {
                    path,
                    // Use "HEAD" for detached HEAD state
                    branch: branch.unwrap_or_else(|| "HEAD".to_string()),
                    status: GitWorktreeStatus {
                        detached,
                        ..Default::default()
                    },
                });
            };

        for line in stdout.lines() {
            if let Some(path_str) = line.strip_prefix("worktree ") {
                // Save previous worktree
                if let Some(path) = current_path.take() {
                    push_worktree(&mut worktrees, path, current_branch.take());
                }
                current_path = Some(PathBuf::from(path_str));
                current_branch = None;
            } else if let Some(branch_ref) = line.strip_prefix("branch ") {
                // Extract branch name from refs/heads/<branch>
                current_branch = Some(
                    branch_ref
                        .strip_prefix("refs/heads/")
                        .unwrap_or(branch_ref)
                        .to_string(),
                );
            }
        }

        // Handle last worktree
        if let Some(path) = current_path {
            push_worktree(&mut worktrees, path, current_branch);
        }

        Ok(worktrees)
    }

    fn is_git_repo(&self) -> bool {
        Command::new("git")
            .current_dir(&self.repo_root)
            .args(["rev-parse", "--git-dir"])
            .output()
            .is_ok_and(|output| output.status.success())
    }

    fn git_pull(&self, path: &Path, strategy: PullStrategy) -> Result<(), WorktreeError> {
        // Check if worktree exists
        if !path.exists() {
            return Err(WorktreeError::WorktreeNotFound {
                path: path.to_path_buf(),
            });
        }

        // Build git pull command based on strategy
        let mut args = vec!["pull"];
        if matches!(strategy, PullStrategy::Rebase) {
            args.push("--rebase");
        }

        let output = Command::new("git").current_dir(path).args(&args).output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitFailed(stderr.trim().to_string()));
        }

        Ok(())
    }

    fn get_status(&self, path: &Path) -> Result<GitWorktreeStatus, WorktreeError> {
        if !path.exists() {
            return Err(WorktreeError::WorktreeNotFound {
                path: path.to_path_buf(),
            });
        }

        // 1. Check dirty status: git status --porcelain
        let status_output = Command::new("git")
            .current_dir(path)
            .args(["status", "--porcelain"])
            .output()?;

        let dirty = if status_output.status.success() {
            parse_git_status_dirty(&String::from_utf8_lossy(&status_output.stdout))
        } else {
            false
        };

        // 2. Check detached HEAD: git rev-parse --abbrev-ref HEAD
        let head_output = Command::new("git")
            .current_dir(path)
            .args(["rev-parse", "--abbrev-ref", "HEAD"])
            .output()?;

        let detached = if head_output.status.success() {
            parse_git_detached(&String::from_utf8_lossy(&head_output.stdout))
        } else {
            false
        };

        // 3. Check ahead/behind: git rev-list --count HEAD..@{u} and @{u}..HEAD
        //    These will fail if no upstream is configured
        let behind_output = Command::new("git")
            .current_dir(path)
            .args(["rev-list", "--count", "HEAD..@{u}"])
            .output()?;

        let ahead_output = Command::new("git")
            .current_dir(path)
            .args(["rev-list", "--count", "@{u}..HEAD"])
            .output()?;

        let (ahead, behind, no_upstream) =
            if behind_output.status.success() && ahead_output.status.success() {
                (
                    parse_rev_list_count(&String::from_utf8_lossy(&ahead_output.stdout)),
                    parse_rev_list_count(&String::from_utf8_lossy(&behind_output.stdout)),
                    false,
                )
            } else {
                // No upstream configured
                (0, 0, true)
            };

        Ok(GitWorktreeStatus {
            dirty,
            ahead,
            behind,
            detached,
            no_upstream,
        })
    }

    fn git_fetch(&self, path: &Path) -> Result<(), WorktreeError> {
        if !path.exists() {
            return Err(WorktreeError::WorktreeNotFound {
                path: path.to_path_buf(),
            });
        }

        let output = Command::new("git")
            .current_dir(path)
            .args(["fetch", "--quiet"])
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitFailed(stderr.trim().to_string()));
        }

        Ok(())
    }
}

#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use rstest::rstest;
    use serial_test::serial;
    use std::process::Command;
    use tempfile::TempDir;

    fn setup_git_repo() -> TempDir {
        let temp_dir = TempDir::new().expect("failed to create temp dir");

        // git init
        Command::new("git")
            .args(["init"])
            .current_dir(temp_dir.path())
            .output()
            .expect("failed to git init");

        // Configure git user for commits
        Command::new("git")
            .args(["config", "user.email", "test@test.com"])
            .current_dir(temp_dir.path())
            .output()
            .expect("failed to set git email");

        Command::new("git")
            .args(["config", "user.name", "Test User"])
            .current_dir(temp_dir.path())
            .output()
            .expect("failed to set git name");

        // Initial commit (required for worktree)
        Command::new("git")
            .args(["commit", "--allow-empty", "-m", "init"])
            .current_dir(temp_dir.path())
            .output()
            .expect("failed to create initial commit");

        temp_dir
    }

    #[test]
    fn new_not_git_repo() {
        let temp_dir = TempDir::new().unwrap();
        let config = WorktreeConfig::default();
        let result = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config);
        assert!(matches!(result, Err(WorktreeError::NotGitRepo(_))));
    }

    #[rstest]
    #[case("/tmp/tazuna-test-worktrees", "/tmp/tazuna-test-worktrees")]
    #[case("relative/path", "relative/path")]
    fn expand_tilde_path_passthrough(#[case] input: &str, #[case] expected: &str) {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig {
            base_path: input.to_string(),
            ..Default::default()
        };
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();
        assert_eq!(manager.base_path(), &PathBuf::from(expected));
    }

    #[test]
    #[serial]
    fn discover_not_git_repo() {
        let temp_dir = TempDir::new().unwrap();
        let config = WorktreeConfig::default();
        let original_cwd = std::env::current_dir().unwrap();

        std::env::set_current_dir(temp_dir.path()).unwrap();
        let result = NativeWorktreeManager::discover(config);
        std::env::set_current_dir(&original_cwd).unwrap();

        assert!(matches!(result, Err(WorktreeError::NotGitRepo(_))));
    }

    #[test]
    fn new_valid_repo() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config);
        assert!(manager.is_ok());
    }

    #[test]
    fn create_worktree_success() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let (path, branch) = manager.create_worktree(None).unwrap();

        assert!(path.exists());
        assert!(path.join(".git").exists()); // worktree marker
        // Path should be under base_path/repo_name/branch (fallback for local repos)
        assert!(path.starts_with(manager.base_path().join(manager.repo_name())));
        // Branch should have prefix + uuid
        assert!(branch.starts_with("tazuna/"));
    }

    #[test]
    fn create_worktree_custom_branch() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let (_, branch) = manager.create_worktree(Some("feature/custom")).unwrap();

        assert_eq!(branch, "feature/custom");
        let worktrees = manager.list_worktrees().unwrap();
        // 1 main + 1 created = 2
        assert_eq!(worktrees.len(), 2);
        let wt = worktrees.iter().find(|w| w.branch == "feature/custom");
        assert!(
            wt.is_some(),
            "Expected worktree with branch 'feature/custom'"
        );
    }

    #[test]
    fn create_worktree_branch_exists() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        // Create first worktree with custom branch
        let _ = manager.create_worktree(Some("feature/test")).unwrap();

        // Try to create second worktree with same branch name
        // With branch-based paths, this returns WorktreeExists (path collision)
        // rather than BranchExists (git error) since path check happens first
        let result = manager.create_worktree(Some("feature/test"));
        assert!(matches!(
            result,
            Err(WorktreeError::BranchExists(_) | WorktreeError::WorktreeExists { .. })
        ));
    }

    #[test]
    fn remove_worktree_success() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let (path, _) = manager.create_worktree(None).unwrap();
        assert!(path.exists());

        manager.remove_worktree(&path).unwrap();
        assert!(!path.exists());
    }

    #[test]
    fn operations_on_nonexistent_path_return_not_found() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();
        let bad_path = Path::new("/nonexistent/path");

        // All path-based operations should return WorktreeNotFound
        assert!(matches!(
            manager.remove_worktree(bad_path),
            Err(WorktreeError::WorktreeNotFound { .. })
        ));
        assert!(matches!(
            manager.git_pull(bad_path, PullStrategy::Merge),
            Err(WorktreeError::WorktreeNotFound { .. })
        ));
        assert!(matches!(
            manager.get_status(bad_path),
            Err(WorktreeError::WorktreeNotFound { .. })
        ));
        assert!(matches!(
            manager.git_fetch(bad_path),
            Err(WorktreeError::WorktreeNotFound { .. })
        ));
    }

    #[test]
    fn list_worktrees_includes_main() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let worktrees = manager.list_worktrees().unwrap();
        // Main worktree is always included
        assert_eq!(worktrees.len(), 1);
        assert_eq!(worktrees[0].path, temp_dir.path());
    }

    #[test]
    fn list_worktrees_multiple() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let _ = manager.create_worktree(None).unwrap();
        let _ = manager.create_worktree(None).unwrap();

        let worktrees = manager.list_worktrees().unwrap();
        // 1 main + 2 created = 3
        assert_eq!(worktrees.len(), 3);
    }

    #[test]
    fn is_git_repo_true() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        assert!(manager.is_git_repo());
    }

    #[test]
    fn branch_naming_with_config_prefix() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig {
            auto_cleanup: false,
            branch_prefix: "wt/".to_string(),
            ..Default::default()
        };
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let (_, branch) = manager.create_worktree(None).unwrap();
        assert!(branch.starts_with("wt/"));

        let worktrees = manager.list_worktrees().unwrap();
        let wt = worktrees.iter().find(|w| w.branch.starts_with("wt/"));
        assert!(wt.is_some(), "Expected worktree with branch prefix 'wt/'");
    }

    #[test]
    fn git_pull_no_remote() {
        // Local repo without remote - git pull will fail
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let (path, _) = manager.create_worktree(None).unwrap();

        // Without a remote, git pull should fail with GitFailed
        let result = manager.git_pull(&path, PullStrategy::Merge);
        assert!(matches!(result, Err(WorktreeError::GitFailed(_))));

        // Same for rebase
        let result = manager.git_pull(&path, PullStrategy::Rebase);
        assert!(matches!(result, Err(WorktreeError::GitFailed(_))));
    }

    #[test]
    #[serial]
    fn discover_from_worktree_finds_main_repo() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();

        // Create manager from main repo
        let main_manager =
            NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config.clone()).unwrap();

        // Create a worktree
        let (worktree_path, _) = main_manager.create_worktree(None).unwrap();

        // Save original cwd to restore later
        let original_cwd = std::env::current_dir().unwrap();

        // Now discover from inside worktree
        std::env::set_current_dir(&worktree_path).unwrap();
        let discovered = NativeWorktreeManager::discover(config).unwrap();

        // Restore cwd before assertions (for cleanup)
        std::env::set_current_dir(&original_cwd).unwrap();

        // CRITICAL: discovered should point to MAIN repo, not worktree
        // Verify by checking repo_name matches main repo's directory name
        let expected_name = temp_dir.path().file_name().unwrap().to_str().unwrap();
        assert_eq!(discovered.repo_name(), expected_name);
    }

    #[test]
    #[serial]
    fn discover_from_main_repo_works() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();

        let original_cwd = std::env::current_dir().unwrap();
        std::env::set_current_dir(temp_dir.path()).unwrap();

        let manager = NativeWorktreeManager::discover(config).unwrap();

        std::env::set_current_dir(&original_cwd).unwrap();

        // Verify by checking repo_name matches main repo's directory name
        let expected_name = temp_dir.path().file_name().unwrap().to_str().unwrap();
        assert_eq!(manager.repo_name(), expected_name);
    }

    #[test]
    #[serial]
    fn list_worktrees_from_worktree_sees_all() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let main_manager =
            NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config.clone()).unwrap();

        // Create worktrees from main
        let (wt1_path, _) = main_manager.create_worktree(None).unwrap();
        let (wt2_path, _) = main_manager.create_worktree(None).unwrap();

        let original_cwd = std::env::current_dir().unwrap();

        // Discover from inside second worktree
        std::env::set_current_dir(&wt2_path).unwrap();
        let discovered = NativeWorktreeManager::discover(config).unwrap();

        std::env::set_current_dir(&original_cwd).unwrap();

        // Must see all worktrees: main + wt1 + wt2 = 3
        let worktrees = discovered.list_worktrees().unwrap();
        assert_eq!(worktrees.len(), 3);

        // Verify paths include both created worktrees
        let paths: Vec<_> = worktrees.iter().map(|w| &w.path).collect();
        assert!(paths.contains(&&wt1_path));
        assert!(paths.contains(&&wt2_path));
    }

    // === Parsing function tests ===

    #[rstest]
    #[case("", false)]
    #[case("   ", false)]
    #[case("\n", false)]
    #[case(" M file.rs", true)]
    #[case("M  file.rs\n", true)]
    #[case("?? newfile.txt", true)]
    #[case("A  staged.rs\n M modified.rs", true)]
    fn parse_git_status_dirty_cases(#[case] input: &str, #[case] expected: bool) {
        assert_eq!(parse_git_status_dirty(input), expected);
    }

    #[rstest]
    #[case("HEAD", true)]
    #[case("HEAD\n", true)]
    #[case("  HEAD  ", true)]
    #[case("main", false)]
    #[case("feature/branch", false)]
    #[case("tazuna/session-123", false)]
    fn parse_git_detached_cases(#[case] input: &str, #[case] expected: bool) {
        assert_eq!(parse_git_detached(input), expected);
    }

    #[rstest]
    #[case("0", 0)]
    #[case("5", 5)]
    #[case("123\n", 123)]
    #[case("  42  ", 42)]
    #[case("", 0)]
    #[case("not a number", 0)]
    #[case("-1", 0)]
    fn parse_rev_list_count_cases(#[case] input: &str, #[case] expected: u32) {
        assert_eq!(parse_rev_list_count(input), expected);
    }

    // === get_status tests ===

    #[test]
    fn get_status_clean_worktree() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let (path, _) = manager.create_worktree(None).unwrap();

        let status = manager.get_status(&path).unwrap();
        assert!(!status.dirty);
        assert!(!status.detached);
        // No upstream since local-only repo
        assert!(status.no_upstream);
        assert_eq!(status.ahead, 0);
        assert_eq!(status.behind, 0);
    }

    #[test]
    fn get_status_dirty_worktree() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let (path, _) = manager.create_worktree(None).unwrap();

        // Create a modified file
        std::fs::write(path.join("dirty.txt"), "content").unwrap();

        let status = manager.get_status(&path).unwrap();
        assert!(status.dirty);
    }

    #[test]
    fn get_status_main_repo() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let status = manager.get_status(temp_dir.path()).unwrap();
        assert!(!status.dirty);
        assert!(!status.detached);
    }

    // === git_fetch tests ===

    #[test]
    fn git_fetch_no_remote() {
        // Local repo without remote - git fetch --quiet succeeds (no-op)
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let (path, _) = manager.create_worktree(None).unwrap();

        // git fetch --quiet returns success even without remote (no-op)
        let result = manager.git_fetch(&path);
        assert!(result.is_ok());
    }

    // === list_worktrees stability test ===

    #[test]
    fn list_worktrees_stable() {
        let temp_dir = setup_git_repo();
        let config = WorktreeConfig::default();
        let manager = NativeWorktreeManager::new(temp_dir.path().to_path_buf(), config).unwrap();

        let (created_path, created_branch) = manager.create_worktree(None).unwrap();

        let list1 = manager.list_worktrees().unwrap();
        let list2 = manager.list_worktrees().unwrap();

        let wt1 = list1.iter().find(|w| w.path == created_path).unwrap();
        let wt2 = list2.iter().find(|w| w.path == created_path).unwrap();

        // Path and branch should be stable across calls
        assert_eq!(wt1.path, wt2.path);
        assert_eq!(wt1.branch, wt2.branch);
        assert_eq!(wt1.branch, created_branch);
    }
}
