//! Handler for the `git` command.

use super::CommandContext;

/// Show compact git status: branch, uncommitted files, ahead/behind remote.
#[allow(clippy::string_slice)] // line[..2] and line[3..]: git porcelain XY status codes are always ASCII
pub(super) fn handle_git(ctx: &CommandContext) -> Option<String> {
    use std::process::Command;

    // Current branch
    let branch = Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .current_dir(ctx.effective_root())
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_else(|| "unknown".to_string());

    // Porcelain status for staged + unstaged changes
    let status_output = Command::new("git")
        .args(["status", "--porcelain"])
        .current_dir(ctx.effective_root())
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
        .unwrap_or_default();

    let changed_files: Vec<&str> = status_output.lines().filter(|l| !l.is_empty()).collect();
    let change_count = changed_files.len();

    // Ahead/behind: --left-right gives "N\tM" (ahead\tbehind)
    let ahead_behind = Command::new("git")
        .args(["rev-list", "--count", "--left-right", "HEAD...@{u}"])
        .current_dir(ctx.effective_root())
        .output()
        .ok()
        .filter(|o| o.status.success())
        .and_then(|o| {
            let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
            let mut parts = s.split_whitespace();
            let ahead: u32 = parts.next()?.parse().ok()?;
            let behind: u32 = parts.next()?.parse().ok()?;
            Some((ahead, behind))
        });

    let mut out = format!("**Branch:** `{branch}`\n");

    if change_count == 0 {
        out.push_str("**Changes:** clean\n");
    } else {
        out.push_str(&format!("**Changes:** {change_count} file(s)\n"));
        for line in &changed_files {
            // Porcelain format: "XY filename" (2-char status + space + path)
            if line.len() > 3 {
                let codes = &line[..2];
                let name = line[3..].trim();
                out.push_str(&format!("  • `{codes}` {name}\n"));
            } else {
                out.push_str(&format!("  • {line}\n"));
            }
        }
    }

    match ahead_behind {
        Some((0, 0)) => out.push_str("**Remote:** up to date\n"),
        Some((ahead, 0)) => out.push_str(&format!("**Remote:** ↑{ahead} ahead\n")),
        Some((0, behind)) => out.push_str(&format!("**Remote:** ↓{behind} behind\n")),
        Some((ahead, behind)) => {
            out.push_str(&format!("**Remote:** ↑{ahead} ahead, ↓{behind} behind\n"));
        }
        None => out.push_str("**Remote:** no tracking branch\n"),
    }

    Some(out)
}

#[cfg(test)]
mod tests {
    use super::super::{CommandDispatch, try_handle_command};

    #[test]
    fn git_command_is_registered() {
        use super::super::commands;
        let found = commands().iter().any(|c| c.name == "git");
        assert!(found, "git command must be in the registry");
    }

    #[test]
    fn git_command_appears_in_help() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy help",
        );
        let output = result.unwrap();
        assert!(
            output.contains("git"),
            "help should list git command: {output}"
        );
    }

    #[test]
    fn git_command_returns_some() {
        // Run from the actual repo root so git commands have a real repo to query.
        let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .unwrap_or(std::path::Path::new("."));
        let services =
            crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
        let room_id = "!test:example.com".to_string();
        let dispatch = CommandDispatch {
            services: &services,
            project_root: &services.project_root,
            bot_user_id: "@timmy:homeserver.local",
            room_id: &room_id,
        };
        let result = try_handle_command(&dispatch, "@timmy git");
        assert!(result.is_some(), "git command should always return Some");
    }

    #[test]
    fn git_command_output_contains_branch() {
        let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .unwrap_or(std::path::Path::new("."));
        let services =
            crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
        let room_id = "!test:example.com".to_string();
        let dispatch = CommandDispatch {
            services: &services,
            project_root: &services.project_root,
            bot_user_id: "@timmy:homeserver.local",
            room_id: &room_id,
        };
        let output = try_handle_command(&dispatch, "@timmy git").unwrap();
        assert!(
            output.contains("**Branch:**"),
            "git output should contain branch info: {output}"
        );
    }

    #[test]
    fn git_command_output_contains_changes() {
        let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .unwrap_or(std::path::Path::new("."));
        let services =
            crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
        let room_id = "!test:example.com".to_string();
        let dispatch = CommandDispatch {
            services: &services,
            project_root: &services.project_root,
            bot_user_id: "@timmy:homeserver.local",
            room_id: &room_id,
        };
        let output = try_handle_command(&dispatch, "@timmy git").unwrap();
        assert!(
            output.contains("**Changes:**"),
            "git output should contain changes section: {output}"
        );
    }

    #[test]
    fn git_command_output_contains_remote() {
        let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .unwrap_or(std::path::Path::new("."));
        let services =
            crate::services::Services::new_test(repo_root.to_path_buf(), "Timmy".to_string());
        let room_id = "!test:example.com".to_string();
        let dispatch = CommandDispatch {
            services: &services,
            project_root: &services.project_root,
            bot_user_id: "@timmy:homeserver.local",
            room_id: &room_id,
        };
        let output = try_handle_command(&dispatch, "@timmy git").unwrap();
        assert!(
            output.contains("**Remote:**"),
            "git output should contain remote section: {output}"
        );
    }

    #[test]
    fn git_command_case_insensitive() {
        let result = super::super::tests::try_cmd_addressed(
            "Timmy",
            "@timmy:homeserver.local",
            "@timmy GIT",
        );
        assert!(result.is_some(), "GIT should match case-insensitively");
    }
}
