//! Thin wrappers around the `git` CLI.
//!
//! Shells out to `git` for current branch, HEAD SHA, remote URL, and other
//! repo metadata. Used to populate CI predefined variables (`CI_COMMIT_SHA`,
//! `CI_COMMIT_BRANCH`, etc.) and to drive the git-worktree container mount.

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

/// Run a git command in the given directory and return trimmed stdout on success.
pub fn git_cmd(dir: &Path, args: &[&str]) -> Option<String> {
    Command::new("git")
        .args(args)
        .current_dir(dir)
        .output()
        .ok()
        .and_then(|o| {
            if o.status.success() {
                Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
            } else {
                None
            }
        })
}

/// Run a git command in the given directory and return raw stdout bytes on success.
///
/// Unlike `git_cmd()` which trims and converts to String, this preserves
/// the exact byte output — needed for piping diffs into `git apply`.
pub fn git_cmd_bytes(dir: &Path, args: &[&str]) -> Option<Vec<u8>> {
    Command::new("git")
        .args(args)
        .current_dir(dir)
        .output()
        .ok()
        .and_then(|o| {
            if o.status.success() {
                Some(o.stdout)
            } else {
                None
            }
        })
}

/// Find the remote name from the current branch's upstream tracking ref,
/// falling back to "origin", then to the first available remote.
pub fn detect_remote_name(dir: &Path) -> String {
    // Try upstream tracking ref: e.g. "perso/main" → "perso"
    if let Some(upstream) = git_cmd(dir, &["rev-parse", "--abbrev-ref", "@{upstream}"])
        && let Some(remote) = upstream.split('/').next()
        && !remote.is_empty()
    {
        return remote.to_string();
    }

    // No upstream — prefer "origin" if it exists, otherwise first remote
    if let Some(remotes) = git_cmd(dir, &["remote"]) {
        let mut first = None;
        for name in remotes.lines() {
            let name = name.trim();
            if name == "origin" {
                return "origin".to_string();
            }
            if first.is_none() && !name.is_empty() {
                first = Some(name.to_string());
            }
        }
        if let Some(f) = first {
            return f;
        }
    }

    "origin".to_string()
}

/// Find the git repository root for a path using `git rev-parse --show-toplevel`.
pub fn find_git_root(path: &Path) -> Option<PathBuf> {
    let dir = if path.is_dir() { path } else { path.parent()? };
    git_cmd(dir, &["rev-parse", "--show-toplevel"]).map(PathBuf::from)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_find_git_root_from_repo() {
        // This test runs inside the cui-cui repo, so it should find the root
        let root = find_git_root(Path::new(".")).expect("should find git root");
        assert!(root.join(".git").exists());
    }

    #[test]
    fn test_find_git_root_from_subdir() {
        // A subdirectory of the repo should return the same root
        let root_from_top = find_git_root(Path::new(".")).unwrap();
        let root_from_sub = find_git_root(Path::new("src")).unwrap();
        assert_eq!(root_from_top, root_from_sub);
    }

    #[test]
    fn test_find_git_root_from_file() {
        // Passing a file path should also work (uses parent dir)
        let root_from_top = find_git_root(Path::new(".")).unwrap();
        let root_from_file = find_git_root(Path::new("src/main.rs")).unwrap();
        assert_eq!(root_from_top, root_from_file);
    }

    #[test]
    fn test_find_git_root_nonexistent_dir() {
        // A path outside any git repo should return None
        assert!(find_git_root(Path::new("/tmp")).is_none());
    }

    #[test]
    fn test_git_cmd_bytes_returns_raw_output() {
        // git rev-parse HEAD should return a commit SHA as raw bytes
        let bytes =
            git_cmd_bytes(Path::new("."), &["rev-parse", "HEAD"]).expect("should get HEAD sha");
        let text = String::from_utf8(bytes).expect("should be valid utf-8");
        // Raw output includes trailing newline (unlike git_cmd which trims)
        assert!(text.ends_with('\n'));
        assert_eq!(text.trim().len(), 40); // full SHA
    }

    #[test]
    fn test_git_cmd_bytes_failure_returns_none() {
        // A failing git command should return None
        assert!(git_cmd_bytes(Path::new("/tmp"), &["rev-parse", "HEAD"]).is_none());
    }
}
