//! Condensed summaries of gate/merge failure output for MCP tool responses
//! and chat notifications.
//!
//! Full gate/test output can run into hundreds of KB; these pure functions
//! reduce it to a bounded, human-scannable digest: the failing gate's name,
//! parsed failing test names (when the output looks like a `cargo test`
//! run), and the last [`SUMMARY_TAIL_LINES`] lines of raw output. No I/O.

use crate::agents::gates::GateFailureKind;
use crate::agents::merge::MergeResult;
use crate::pipeline_state::MergeFailureKind;
use crate::service::notifications::format::truncate_gate_output;

/// Number of trailing output lines retained in a gate-failure summary.
///
/// Kept a few lines under `notifications::format::MERGE_FAILURE_TAIL_LINES`
/// (30): chat notifications re-apply that cap on top of this summary's
/// `Failing gate:`/`Failing tests:`/`--- output tail ---` header lines, and
/// the total must stay at or under 30 lines so that second pass is a no-op
/// instead of truncating the header off the front of an already-shaped
/// summary.
pub const SUMMARY_TAIL_LINES: usize = 25;

/// Parse `cargo test` output for the list of failing test names.
///
/// libtest prints one or more `failures:` sections listing the fully
/// qualified name of each failing test on its own indented line, terminated
/// by a blank line or the `test result:` summary line. Returns names in
/// first-seen order with duplicates removed (a name can repeat across the
/// `failures:` section and the final summary re-listing).
pub fn extract_failing_test_names(output: &str) -> Vec<String> {
    let mut names = Vec::new();
    let mut in_failures = false;
    for line in output.lines() {
        let trimmed = line.trim();
        if trimmed == "failures:" {
            in_failures = true;
            continue;
        }
        if in_failures {
            if trimmed.is_empty() || trimmed.starts_with("test result:") {
                in_failures = false;
                continue;
            }
            if !names.iter().any(|n: &String| n == trimmed) {
                names.push(trimmed.to_string());
            }
        }
    }
    names
}

/// Human-readable label for the gate/step that produced a [`MergeResult`].
fn failing_gate_label(result: &MergeResult) -> &'static str {
    match result {
        MergeResult::Success { .. } => "none",
        MergeResult::Conflict { .. } => "git merge",
        MergeResult::GateFailure { failure_kind, .. } => match failure_kind {
            Some(GateFailureKind::Fmt) => "fmt",
            Some(GateFailureKind::Lint) => "clippy",
            Some(GateFailureKind::Test) => "tests",
            Some(GateFailureKind::SourceMapCheck) => "source-map-check",
            Some(GateFailureKind::ContentConflict) => "git merge",
            Some(GateFailureKind::Build) => "build",
            Some(GateFailureKind::Other) | None => "unknown",
        },
        MergeResult::NoCommits { .. } => "no commits",
        MergeResult::Other { .. } => "unknown",
    }
}

/// Human-readable label for the gate/step that produced a [`MergeFailureKind`].
fn failing_gate_label_for_kind(kind: &MergeFailureKind) -> &'static str {
    match kind {
        MergeFailureKind::ConflictDetected(_) => "git merge",
        MergeFailureKind::GatesFailed(_) => "quality gates",
        MergeFailureKind::EmptyDiff => "empty diff",
        MergeFailureKind::NoCommits => "no commits",
        MergeFailureKind::Other(_) => "unknown",
    }
}

/// Build a condensed summary: a `Failing gate:` line, an optional
/// `Failing tests:` line (when any are parseable), and the last
/// [`SUMMARY_TAIL_LINES`] lines of `output`.
fn summarize(label: &str, output: &str) -> String {
    let mut summary = format!("Failing gate: {label}\n");
    let test_names = extract_failing_test_names(output);
    if !test_names.is_empty() {
        summary.push_str(&format!("Failing tests: {}\n", test_names.join(", ")));
    }
    summary.push_str("--- output tail ---\n");
    summary.push_str(&truncate_gate_output(output, SUMMARY_TAIL_LINES));
    summary
}

/// Summarize a completed [`MergeResult`] for MCP tool responses.
///
/// Returns an empty string for [`MergeResult::Success`] — successful merges
/// carry no gate-failure summary.
pub fn summarize_merge_result(result: &MergeResult) -> String {
    if matches!(result, MergeResult::Success { .. }) {
        return String::new();
    }
    summarize(failing_gate_label(result), result.output())
}

/// Summarize a [`MergeFailureKind`] for chat notifications, so the same
/// bounded shaping (label + parsed test names + output tail) reaches the
/// room instead of the raw gate/conflict output that `display_reason`
/// embeds verbatim.
pub fn summarize_merge_failure_kind(kind: &MergeFailureKind) -> String {
    summarize(failing_gate_label_for_kind(kind), &kind.to_gate_output())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::agents::gates::GateFailureKind;

    // ── extract_failing_test_names ────────────────────────────────────────────

    #[test]
    fn extract_failing_test_names_single_failures_section() {
        let output = "running 3 tests\ntest foo::bar ... FAILED\ntest foo::baz ... ok\n\n\
             failures:\n    foo::bar\n\ntest result: FAILED. 1 passed; 1 failed; 0 ignored\n";
        assert_eq!(extract_failing_test_names(output), vec!["foo::bar"]);
    }

    #[test]
    fn extract_failing_test_names_dedupes_across_sections() {
        // cargo test prints the failing list once mid-output and again in the
        // final "failures:" summary block; both must collapse to one entry.
        let output = "failures:\n    foo::bar\n\nfailures:\n    foo::bar\n\ntest result: FAILED.\n";
        assert_eq!(extract_failing_test_names(output), vec!["foo::bar"]);
    }

    #[test]
    fn extract_failing_test_names_multiple_names() {
        let output = "failures:\n    a::one\n    b::two\n\ntest result: FAILED.\n";
        assert_eq!(extract_failing_test_names(output), vec!["a::one", "b::two"]);
    }

    #[test]
    fn extract_failing_test_names_empty_when_no_failures_section() {
        let output = "test result: ok. 3 passed; 0 failed\n";
        assert!(extract_failing_test_names(output).is_empty());
    }

    #[test]
    fn extract_failing_test_names_empty_on_build_error_output() {
        let output = "error[E0063]: missing field `plan` in initializer of `Stage`\n";
        assert!(extract_failing_test_names(output).is_empty());
    }

    // ── summarize_merge_result ─────────────────────────────────────────────────

    #[test]
    fn summarize_merge_result_success_is_empty() {
        let result = MergeResult::Success {
            conflicts_resolved: false,
            conflict_details: None,
            gate_output: "all good".to_string(),
        };
        assert_eq!(summarize_merge_result(&result), "");
    }

    #[test]
    fn summarize_merge_result_build_error_names_the_gate() {
        let result = MergeResult::GateFailure {
            output: "error[E0063]: missing field `plan`".to_string(),
            failure_kind: Some(GateFailureKind::Build),
        };
        let summary = summarize_merge_result(&result);
        assert!(summary.starts_with("Failing gate: build\n"));
        assert!(summary.contains("error[E0063]"));
        assert!(!summary.contains("Failing tests:"));
    }

    #[test]
    fn summarize_merge_result_test_failure_includes_names() {
        let output = "running 1 test\ntest foo::bar ... FAILED\n\n\
             failures:\n    foo::bar\n\ntest result: FAILED. 0 passed; 1 failed\n";
        let result = MergeResult::GateFailure {
            output: output.to_string(),
            failure_kind: Some(GateFailureKind::Test),
        };
        let summary = summarize_merge_result(&result);
        assert!(summary.starts_with("Failing gate: tests\n"));
        assert!(summary.contains("Failing tests: foo::bar\n"));
    }

    #[test]
    fn summarize_merge_result_conflict_names_git_merge() {
        let result = MergeResult::Conflict {
            details: Some("CONFLICT in src/lib.rs".to_string()),
            output: "merge failed".to_string(),
        };
        let summary = summarize_merge_result(&result);
        assert!(summary.starts_with("Failing gate: git merge\n"));
        assert!(summary.contains("merge failed"));
    }

    #[test]
    fn summarize_merge_result_truncates_long_output() {
        let lines: Vec<String> = (1..=100).map(|i| format!("line{i}")).collect();
        let output = lines.join("\n");
        let result = MergeResult::GateFailure {
            output,
            failure_kind: Some(GateFailureKind::Test),
        };
        let summary = summarize_merge_result(&result);
        assert!(summary.contains("output truncated"));
        assert!(summary.contains("line100"));
        assert!(
            !summary.contains("line1\n"),
            "line1 should have been dropped by the tail"
        );
    }

    // ── summarize_merge_failure_kind ───────────────────────────────────────────

    #[test]
    fn summarize_merge_failure_kind_gates_failed() {
        let kind = MergeFailureKind::GatesFailed("test result: FAILED. 1 failed".to_string());
        let summary = summarize_merge_failure_kind(&kind);
        assert!(summary.starts_with("Failing gate: quality gates\n"));
        assert!(summary.contains("test result: FAILED"));
    }

    #[test]
    fn summarize_merge_failure_kind_does_not_include_raw_prefix_sentence() {
        // display_reason() would prefix "Quality gates failed: "; the summary
        // must not duplicate that human-sentence framing.
        let kind = MergeFailureKind::GatesFailed("boom".to_string());
        let summary = summarize_merge_failure_kind(&kind);
        assert!(!summary.contains("Quality gates failed:"));
    }
}
