//! MCP git tools — status, diff, add, commit, and log operations on agent worktrees.
//!
//! This file is a thin adapter: it deserialises MCP payloads, delegates to
//! `crate::service::git_ops` for all business logic, and serialises responses.
use crate::http::context::AppContext;
use serde_json::{Value, json};
use std::path::PathBuf;

/// Validates that `worktree_path` exists and is inside the project's
/// `.huskies/worktrees/` directory. Returns the canonicalized path.
///
/// Thin wrapper that obtains the project root from `ctx` and delegates to
/// `service::git_ops::io::validate_worktree_path`.
fn validate_worktree_path(worktree_path: &str, ctx: &AppContext) -> Result<PathBuf, String> {
    let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
    crate::service::git_ops::io::validate_worktree_path(worktree_path, &project_root)
        .map_err(|e| e.to_string())
}

/// Run a git command in the given directory and return its output.
async fn run_git(args: Vec<&'static str>, dir: PathBuf) -> Result<std::process::Output, String> {
    crate::service::git_ops::io::run_git(args, dir)
        .await
        .map_err(|e| e.to_string())
}

/// Run a git command with owned args in the given directory.
async fn run_git_owned(args: Vec<String>, dir: PathBuf) -> Result<std::process::Output, String> {
    crate::service::git_ops::io::run_git_owned(args, dir)
        .await
        .map_err(|e| e.to_string())
}

/// git_status — returns working tree status (staged, unstaged, untracked files).
pub(super) async fn tool_git_status(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let worktree_path = args
        .get("worktree_path")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: worktree_path")?;

    let dir = validate_worktree_path(worktree_path, ctx)?;

    let output = run_git(vec!["status", "--porcelain=v1", "-u"], dir).await?;

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

    if !output.status.success() {
        return Err(format!(
            "git status failed (exit {}): {stderr}",
            output.status.code().unwrap_or(-1)
        ));
    }

    let (staged, unstaged, untracked) =
        crate::service::git_ops::parse_git_status_porcelain(&stdout);

    serde_json::to_string_pretty(&json!({
        "staged": staged,
        "unstaged": unstaged,
        "untracked": untracked,
        "clean": staged.is_empty() && unstaged.is_empty() && untracked.is_empty(),
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

/// git_diff — returns diff output. Supports staged/unstaged/commit range.
pub(super) async fn tool_git_diff(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let worktree_path = args
        .get("worktree_path")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: worktree_path")?;

    let dir = validate_worktree_path(worktree_path, ctx)?;

    let staged = args
        .get("staged")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);

    let commit_range = args
        .get("commit_range")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    let mut git_args: Vec<String> = vec!["diff".to_string()];

    if staged {
        git_args.push("--staged".to_string());
    }

    if let Some(range) = commit_range {
        git_args.push(range);
    }

    let output = run_git_owned(git_args, dir).await?;

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

    if !output.status.success() {
        return Err(format!(
            "git diff failed (exit {}): {stderr}",
            output.status.code().unwrap_or(-1)
        ));
    }

    serde_json::to_string_pretty(&json!({
        "diff": stdout.as_ref(),
        "exit_code": output.status.code().unwrap_or(-1),
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

/// git_add — stages files by path.
pub(super) async fn tool_git_add(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let worktree_path = args
        .get("worktree_path")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: worktree_path")?;

    let paths: Vec<String> = args
        .get("paths")
        .and_then(|v| v.as_array())
        .ok_or("Missing required argument: paths (must be an array of strings)")?
        .iter()
        .filter_map(|v| v.as_str().map(|s| s.to_string()))
        .collect();

    if paths.is_empty() {
        return Err("paths must be a non-empty array of strings".to_string());
    }

    let dir = validate_worktree_path(worktree_path, ctx)?;

    let mut git_args: Vec<String> = vec!["add".to_string(), "--".to_string()];
    git_args.extend(paths.clone());

    let output = run_git_owned(git_args, dir).await?;

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

    if !output.status.success() {
        return Err(format!(
            "git add failed (exit {}): {stderr}",
            output.status.code().unwrap_or(-1)
        ));
    }

    serde_json::to_string_pretty(&json!({
        "staged": paths,
        "exit_code": output.status.code().unwrap_or(0),
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

/// git_commit — commits staged changes with a message.
pub(super) async fn tool_git_commit(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let worktree_path = args
        .get("worktree_path")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: worktree_path")?;

    let message = args
        .get("message")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: message")?
        .to_string();

    if message.trim().is_empty() {
        return Err("message must not be empty".to_string());
    }

    let dir = validate_worktree_path(worktree_path, ctx)?;

    let git_args: Vec<String> = vec!["commit".to_string(), "--message".to_string(), message];

    let output = run_git_owned(git_args, dir).await?;

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

    if !output.status.success() {
        return Err(format!(
            "git commit failed (exit {}): {stderr}",
            output.status.code().unwrap_or(-1)
        ));
    }

    serde_json::to_string_pretty(&json!({
        "output": stdout.as_ref(),
        "exit_code": output.status.code().unwrap_or(0),
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

/// git_log — returns commit history with configurable count and format.
pub(super) async fn tool_git_log(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let worktree_path = args
        .get("worktree_path")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: worktree_path")?;

    let dir = validate_worktree_path(worktree_path, ctx)?;

    let count = args
        .get("count")
        .and_then(|v| v.as_u64())
        .unwrap_or(10)
        .min(500);

    let format = args
        .get("format")
        .and_then(|v| v.as_str())
        .unwrap_or("%H%x09%s%x09%an%x09%ai")
        .to_string();

    let git_args: Vec<String> = vec![
        "log".to_string(),
        format!("--max-count={count}"),
        format!("--pretty=format:{format}"),
    ];

    let output = run_git_owned(git_args, dir).await?;

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

    if !output.status.success() {
        return Err(format!(
            "git log failed (exit {}): {stderr}",
            output.status.code().unwrap_or(-1)
        ));
    }

    serde_json::to_string_pretty(&json!({
        "log": stdout.as_ref(),
        "exit_code": output.status.code().unwrap_or(0),
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::context::AppContext;
    use crate::http::test_helpers::test_ctx;
    use serde_json::json;

    /// Create a temp directory with a git worktree structure and init a repo.
    fn setup_worktree() -> (tempfile::TempDir, PathBuf, AppContext) {
        let tmp = tempfile::tempdir().unwrap();
        let story_wt = tmp
            .path()
            .join(".huskies")
            .join("worktrees")
            .join("42_test_story");
        std::fs::create_dir_all(&story_wt).unwrap();

        // Init git repo in the worktree
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(&story_wt)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.email", "test@test.com"])
            .current_dir(&story_wt)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.name", "Test"])
            .current_dir(&story_wt)
            .output()
            .unwrap();

        let ctx = test_ctx(tmp.path());
        (tmp, story_wt, ctx)
    }

    // ── validate_worktree_path ─────────────────────────────────────────

    #[test]
    fn validate_rejects_relative_path() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = validate_worktree_path("relative/path", &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("absolute"));
    }

    #[test]
    fn validate_rejects_nonexistent_path() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = validate_worktree_path("/nonexistent_path_xyz_git", &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("does not exist"));
    }

    #[test]
    fn validate_rejects_path_outside_worktrees() {
        let tmp = tempfile::tempdir().unwrap();
        let wt_dir = tmp.path().join(".huskies").join("worktrees");
        std::fs::create_dir_all(&wt_dir).unwrap();
        let ctx = test_ctx(tmp.path());
        let result = validate_worktree_path(tmp.path().to_str().unwrap(), &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("inside .huskies/worktrees"));
    }

    #[test]
    fn validate_accepts_path_inside_worktrees() {
        let tmp = tempfile::tempdir().unwrap();
        let story_wt = tmp
            .path()
            .join(".huskies")
            .join("worktrees")
            .join("42_test_story");
        std::fs::create_dir_all(&story_wt).unwrap();
        let ctx = test_ctx(tmp.path());
        let result = validate_worktree_path(story_wt.to_str().unwrap(), &ctx);
        assert!(result.is_ok(), "expected Ok, got: {:?}", result);
    }

    // ── git_status ────────────────────────────────────────────────────

    #[tokio::test]
    async fn git_status_missing_worktree_path() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_git_status(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("worktree_path"));
    }

    #[tokio::test]
    async fn git_status_clean_repo() {
        let (_tmp, story_wt, ctx) = setup_worktree();

        // Make an initial commit so HEAD exists
        std::fs::write(story_wt.join("readme.txt"), "hello").unwrap();
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(&story_wt)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "init"])
            .current_dir(&story_wt)
            .output()
            .unwrap();

        let result = tool_git_status(&json!({"worktree_path": story_wt.to_str().unwrap()}), &ctx)
            .await
            .unwrap();

        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["clean"], true);
        assert!(parsed["staged"].as_array().unwrap().is_empty());
        assert!(parsed["unstaged"].as_array().unwrap().is_empty());
        assert!(parsed["untracked"].as_array().unwrap().is_empty());
    }

    #[tokio::test]
    async fn git_status_shows_untracked_file() {
        let (_tmp, story_wt, ctx) = setup_worktree();

        // Make initial commit
        std::fs::write(story_wt.join("readme.txt"), "hello").unwrap();
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(&story_wt)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "init"])
            .current_dir(&story_wt)
            .output()
            .unwrap();

        // Add untracked file
        std::fs::write(story_wt.join("new_file.txt"), "content").unwrap();

        let result = tool_git_status(&json!({"worktree_path": story_wt.to_str().unwrap()}), &ctx)
            .await
            .unwrap();

        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["clean"], false);
        let untracked = parsed["untracked"].as_array().unwrap();
        assert!(
            untracked
                .iter()
                .any(|v| v.as_str().unwrap().contains("new_file.txt")),
            "expected new_file.txt in untracked: {parsed}"
        );
    }

    // ── git_diff ──────────────────────────────────────────────────────

    #[tokio::test]
    async fn git_diff_missing_worktree_path() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_git_diff(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("worktree_path"));
    }

    #[tokio::test]
    async fn git_diff_returns_diff() {
        let (_tmp, story_wt, ctx) = setup_worktree();

        // Create initial commit
        std::fs::write(story_wt.join("file.txt"), "line1\n").unwrap();
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(&story_wt)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "init"])
            .current_dir(&story_wt)
            .output()
            .unwrap();

        // Modify file (unstaged)
        std::fs::write(story_wt.join("file.txt"), "line1\nline2\n").unwrap();

        let result = tool_git_diff(&json!({"worktree_path": story_wt.to_str().unwrap()}), &ctx)
            .await
            .unwrap();

        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert!(
            parsed["diff"].as_str().unwrap().contains("line2"),
            "expected diff output: {parsed}"
        );
    }

    #[tokio::test]
    async fn git_diff_staged_flag() {
        let (_tmp, story_wt, ctx) = setup_worktree();

        // Create initial commit
        std::fs::write(story_wt.join("file.txt"), "line1\n").unwrap();
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(&story_wt)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "init"])
            .current_dir(&story_wt)
            .output()
            .unwrap();

        // Stage a modification
        std::fs::write(story_wt.join("file.txt"), "line1\nstaged_change\n").unwrap();
        std::process::Command::new("git")
            .args(["add", "file.txt"])
            .current_dir(&story_wt)
            .output()
            .unwrap();

        let result = tool_git_diff(
            &json!({"worktree_path": story_wt.to_str().unwrap(), "staged": true}),
            &ctx,
        )
        .await
        .unwrap();

        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert!(
            parsed["diff"].as_str().unwrap().contains("staged_change"),
            "expected staged diff: {parsed}"
        );
    }

    // ── git_add ───────────────────────────────────────────────────────

    #[tokio::test]
    async fn git_add_missing_worktree_path() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_git_add(&json!({"paths": ["file.txt"]}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("worktree_path"));
    }

    #[tokio::test]
    async fn git_add_missing_paths() {
        let (_tmp, story_wt, ctx) = setup_worktree();
        let result =
            tool_git_add(&json!({"worktree_path": story_wt.to_str().unwrap()}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("paths"));
    }

    #[tokio::test]
    async fn git_add_empty_paths() {
        let (_tmp, story_wt, ctx) = setup_worktree();
        let result = tool_git_add(
            &json!({"worktree_path": story_wt.to_str().unwrap(), "paths": []}),
            &ctx,
        )
        .await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("non-empty"));
    }

    #[tokio::test]
    async fn git_add_stages_file() {
        let (_tmp, story_wt, ctx) = setup_worktree();

        std::fs::write(story_wt.join("file.txt"), "content").unwrap();

        let result = tool_git_add(
            &json!({
                "worktree_path": story_wt.to_str().unwrap(),
                "paths": ["file.txt"]
            }),
            &ctx,
        )
        .await
        .unwrap();

        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["exit_code"], 0);
        let staged = parsed["staged"].as_array().unwrap();
        assert!(staged.iter().any(|v| v.as_str().unwrap() == "file.txt"));

        // Verify file is actually staged
        let status = std::process::Command::new("git")
            .args(["status", "--porcelain"])
            .current_dir(&story_wt)
            .output()
            .unwrap();
        let output = String::from_utf8_lossy(&status.stdout);
        assert!(
            output.contains("A  file.txt"),
            "file should be staged: {output}"
        );
    }

    // ── git_commit ────────────────────────────────────────────────────

    #[tokio::test]
    async fn git_commit_missing_worktree_path() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_git_commit(&json!({"message": "test"}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("worktree_path"));
    }

    #[tokio::test]
    async fn git_commit_missing_message() {
        let (_tmp, story_wt, ctx) = setup_worktree();
        let result =
            tool_git_commit(&json!({"worktree_path": story_wt.to_str().unwrap()}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("message"));
    }

    #[tokio::test]
    async fn git_commit_empty_message() {
        let (_tmp, story_wt, ctx) = setup_worktree();
        let result = tool_git_commit(
            &json!({"worktree_path": story_wt.to_str().unwrap(), "message": "   "}),
            &ctx,
        )
        .await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("empty"));
    }

    #[tokio::test]
    async fn git_commit_creates_commit() {
        let (_tmp, story_wt, ctx) = setup_worktree();

        // Stage a file
        std::fs::write(story_wt.join("file.txt"), "content").unwrap();
        std::process::Command::new("git")
            .args(["add", "file.txt"])
            .current_dir(&story_wt)
            .output()
            .unwrap();

        let result = tool_git_commit(
            &json!({
                "worktree_path": story_wt.to_str().unwrap(),
                "message": "test commit message"
            }),
            &ctx,
        )
        .await
        .unwrap();

        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["exit_code"], 0);

        // Verify commit exists
        let log = std::process::Command::new("git")
            .args(["log", "--oneline"])
            .current_dir(&story_wt)
            .output()
            .unwrap();
        let log_output = String::from_utf8_lossy(&log.stdout);
        assert!(
            log_output.contains("test commit message"),
            "expected commit in log: {log_output}"
        );
    }

    // ── git_log ───────────────────────────────────────────────────────

    #[tokio::test]
    async fn git_log_missing_worktree_path() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_git_log(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("worktree_path"));
    }

    #[tokio::test]
    async fn git_log_returns_history() {
        let (_tmp, story_wt, ctx) = setup_worktree();

        // Make a commit
        std::fs::write(story_wt.join("file.txt"), "content").unwrap();
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(&story_wt)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "first commit"])
            .current_dir(&story_wt)
            .output()
            .unwrap();

        let result = tool_git_log(&json!({"worktree_path": story_wt.to_str().unwrap()}), &ctx)
            .await
            .unwrap();

        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["exit_code"], 0);
        assert!(
            parsed["log"].as_str().unwrap().contains("first commit"),
            "expected commit in log: {parsed}"
        );
    }

    #[tokio::test]
    async fn git_log_respects_count() {
        let (_tmp, story_wt, ctx) = setup_worktree();

        // Make multiple commits
        for i in 0..5 {
            std::fs::write(story_wt.join("file.txt"), format!("content {i}")).unwrap();
            std::process::Command::new("git")
                .args(["add", "."])
                .current_dir(&story_wt)
                .output()
                .unwrap();
            std::process::Command::new("git")
                .args(["commit", "-m", &format!("commit {i}")])
                .current_dir(&story_wt)
                .output()
                .unwrap();
        }

        let result = tool_git_log(
            &json!({"worktree_path": story_wt.to_str().unwrap(), "count": 2}),
            &ctx,
        )
        .await
        .unwrap();

        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        // With count=2, only 2 commit entries should appear
        let log = parsed["log"].as_str().unwrap();
        // Each log line is tab-separated; count newlines
        let lines: Vec<&str> = log.lines().collect();
        assert_eq!(lines.len(), 2, "expected 2 log entries, got: {log}");
    }
}
