//! Handler for the `diff` command.
//!
//! Shows the git diff from the configured main branch to the story's worktree
//! HEAD, formatted for readability in chat.

use super::CommandContext;
use crate::chat::util::truncate_at_char_boundary;
use std::path::Path;
use std::process::Command;

/// Display the git diff from the configured main branch to a story's worktree HEAD.
///
/// Usage: `diff <number>`
pub(super) fn handle_diff(ctx: &CommandContext) -> Option<String> {
    let num_str = ctx.args.trim();
    if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
        return None;
    }

    let story_id = match find_story_id(num_str) {
        Some(id) => id,
        None => {
            return Some(format!(
                "No story with number **{num_str}** found in the pipeline."
            ));
        }
    };

    let wt_path = crate::worktree::worktree_path(ctx.effective_root(), &story_id);
    if !wt_path.is_dir() {
        return Some(format!(
            "Story **{num_str}** has no worktree. The diff is only available once a coder has started working on it."
        ));
    }

    let base_branch = resolve_base_branch(ctx.effective_root());
    let range = format!("{base_branch}...HEAD");

    let stat = run_git(&wt_path, &["diff", "--stat", &range]);
    let diff = run_git(&wt_path, &["diff", &range]);

    let mut out = format!("## Diff — story {num_str} vs `{base_branch}`\n\n");

    if stat.is_empty() && diff.is_empty() {
        out.push_str("*(no changes relative to main branch)*\n");
        return Some(out);
    }

    if !stat.is_empty() {
        out.push_str("**Changed files:**\n```\n");
        out.push_str(&stat);
        out.push_str("\n```\n\n");
    }

    if !diff.is_empty() {
        const MAX_DIFF_BYTES: usize = 8_000;
        if diff.len() > MAX_DIFF_BYTES {
            let truncated = truncate_at_char_boundary(&diff, MAX_DIFF_BYTES);
            out.push_str("**Diff** *(truncated — showing first 8 KB)*:\n```diff\n");
            out.push_str(truncated);
            out.push_str("\n... (truncated)\n```\n");
        } else {
            out.push_str("**Diff:**\n```diff\n");
            out.push_str(&diff);
            out.push_str("\n```\n");
        }
    }

    Some(out)
}

/// Find the story_id in the pipeline whose numeric prefix matches `num_str`.
fn find_story_id(num_str: &str) -> Option<String> {
    let items = crate::pipeline_state::read_all_typed();
    items.into_iter().find_map(|item| {
        let file_num = item
            .story_id
            .0
            .split('_')
            .next()
            .filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()))
            .unwrap_or("");
        if file_num == num_str {
            Some(item.story_id.0.clone())
        } else {
            None
        }
    })
}

/// Return the configured base branch, or auto-detect it from the project root HEAD.
fn resolve_base_branch(project_root: &Path) -> String {
    let configured = crate::config::ProjectConfig::load(project_root)
        .ok()
        .and_then(|c| c.base_branch);
    crate::worktree::resolve_base_branch(project_root, configured.as_deref())
}

/// Run a git command in `dir`, returning trimmed stdout (empty string on failure).
fn run_git(dir: &Path, args: &[&str]) -> String {
    Command::new("git")
        .args(args)
        .current_dir(dir)
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default()
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    use super::super::{CommandDispatch, try_handle_command};

    fn diff_cmd(root: &std::path::Path, args: &str) -> Option<String> {
        let services = crate::services::Services::new_test(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,
        };
        try_handle_command(&dispatch, &format!("@timmy diff {args}"))
    }

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

    #[test]
    fn diff_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("diff"),
            "help should list diff command: {output}"
        );
    }

    #[test]
    fn diff_command_no_args_routes_to_timmy() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = diff_cmd(tmp.path(), "");
        assert!(
            result.is_none(),
            "no args should route to LLM (None): {result:?}"
        );
    }

    #[test]
    fn diff_command_non_numeric_routes_to_timmy() {
        let tmp = tempfile::TempDir::new().unwrap();
        let result = diff_cmd(tmp.path(), "the login feature branch");
        assert!(
            result.is_none(),
            "natural-language args should route to LLM (None): {result:?}"
        );
    }

    #[test]
    fn diff_command_well_formed_runs_handler() {
        crate::db::ensure_content_store();
        let tmp = tempfile::TempDir::new().unwrap();
        let result = diff_cmd(tmp.path(), "99994");
        assert!(
            result.is_some(),
            "well-formed numeric arg should run the command handler: {result:?}"
        );
    }

    #[test]
    fn diff_command_story_not_found_returns_friendly_message() {
        crate::db::ensure_content_store();
        let tmp = tempfile::TempDir::new().unwrap();
        let output = diff_cmd(tmp.path(), "99993").unwrap();
        assert!(
            output.contains("99993"),
            "message should include story number: {output}"
        );
        assert!(
            output.contains("found") || output.contains("pipeline"),
            "message should explain not found: {output}"
        );
    }

    #[test]
    fn diff_command_no_worktree_returns_clear_error() {
        use crate::chat::test_helpers::write_story_file;
        let tmp = tempfile::TempDir::new().unwrap();
        write_story_file(
            tmp.path(),
            "2_current",
            "55551_story_no_worktree.md",
            "---\nname: No Worktree\n---\n",
            Some("No Worktree"),
        );
        let output = diff_cmd(tmp.path(), "55551").unwrap();
        assert!(
            output.contains("worktree")
                || output.contains("no worktree")
                || output.contains("Worktree"),
            "should report missing worktree: {output}"
        );
    }

    #[test]
    fn truncate_at_char_boundary_short_string() {
        let s = "hello";
        assert_eq!(truncate_at_char_boundary(s, 100), "hello");
    }

    #[test]
    fn truncate_at_char_boundary_exact_limit() {
        let s = "hello";
        assert_eq!(truncate_at_char_boundary(s, 5), "hello");
    }

    #[test]
    fn truncate_at_char_boundary_over_limit() {
        let s = "hello world";
        assert_eq!(truncate_at_char_boundary(s, 5), "hello");
    }
}
