//! Filters cargo output — build errors, test results, clippy warnings.

use crate::core::runner;
use crate::core::stream::{BlockHandler, BlockStreamFilter, StreamFilter};
use crate::core::truncate::{CAP_ERRORS, CAP_LIST, CAP_WARNINGS};
use crate::core::utils::{resolved_command, truncate};
use anyhow::Result;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::ffi::OsString;
use std::sync::OnceLock;

#[derive(Debug, Clone)]
pub enum CargoCommand {
    Build,
    Test,
    Clippy,
    Check,
    Install,
    Nextest,
}

pub fn run(cmd: CargoCommand, args: &[String], verbose: u8) -> Result<i32> {
    match cmd {
        CargoCommand::Build => run_build(args, verbose),
        CargoCommand::Test => run_test(args, verbose),
        CargoCommand::Clippy => run_clippy(args, verbose),
        CargoCommand::Check => run_check(args, verbose),
        CargoCommand::Install => run_install(args, verbose),
        CargoCommand::Nextest => run_nextest(args, verbose),
    }
}

/// Reconstruct args with `--` separator preserved from the original command line.
/// Clap strips `--` from parsed args, but cargo subcommands need it to separate
/// their own flags from test runner flags (e.g. `cargo test -- --nocapture`).
fn restore_double_dash(args: &[String]) -> Vec<String> {
    let raw_args: Vec<String> = std::env::args().collect();
    restore_double_dash_with_raw(args, &raw_args)
}

/// Testable version that takes raw_args explicitly.
fn restore_double_dash_with_raw(args: &[String], raw_args: &[String]) -> Vec<String> {
    if args.is_empty() {
        return args.to_vec();
    }

    // If args already contain `--` (Clap preserved it), no restoration needed
    if args.iter().any(|a| a == "--") {
        return args.to_vec();
    }

    // Find `--` in the original command line
    let sep_pos = match raw_args.iter().position(|a| a == "--") {
        Some(pos) => pos,
        None => return args.to_vec(),
    };

    // Count how many of our parsed args appeared before `--` in the original.
    // Args before `--` are positional (e.g. test name), args after are flags.
    let args_before_sep = raw_args[..sep_pos]
        .iter()
        .filter(|a| args.contains(a))
        .count();

    let mut result = Vec::with_capacity(args.len() + 1);
    result.extend_from_slice(&args[..args_before_sep]);
    result.push("--".to_string());
    result.extend_from_slice(&args[args_before_sep..]);
    result
}

// --- Stream handlers ---

struct CargoBuildHandler {
    compiled: usize,
    warnings: usize,
    error_count: usize,
    finished_line: Option<String>,
}

impl CargoBuildHandler {
    fn new() -> Self {
        Self {
            compiled: 0,
            warnings: 0,
            error_count: 0,
            finished_line: None,
        }
    }
}

impl BlockHandler for CargoBuildHandler {
    fn should_skip(&mut self, line: &str) -> bool {
        let trimmed = line.trim_start();
        if trimmed.starts_with("Compiling") || trimmed.starts_with("Checking") {
            self.compiled += 1;
            return true;
        }
        if trimmed.starts_with("Downloading") || trimmed.starts_with("Downloaded") {
            return true;
        }
        if trimmed.starts_with("Finished") {
            self.finished_line = Some(trimmed.to_string());
            return true;
        }
        if line.starts_with("warning:") && line.contains("generated") && line.contains("warning") {
            return true;
        }
        if (line.starts_with("error:") || line.starts_with("error["))
            && (line.contains("aborting due to") || line.contains("could not compile"))
        {
            return true;
        }
        false
    }

    fn is_block_start(&mut self, line: &str) -> bool {
        if line.starts_with("error[") || line.starts_with("error:") {
            self.error_count += 1;
            return true;
        }
        if line.starts_with("warning:") || line.starts_with("warning[") {
            self.warnings += 1;
            return true;
        }
        false
    }

    fn is_block_continuation(&mut self, line: &str, block: &[String]) -> bool {
        !(line.trim().is_empty() && block.len() > 3)
    }

    fn format_summary(&self, _exit_code: i32, _raw: &str) -> Option<String> {
        if self.error_count == 0 && self.warnings == 0 {
            let mut s = format!("cargo build ({} crates compiled)", self.compiled);
            if let Some(ref finished) = self.finished_line {
                s = format!("{}\n{}", s, finished);
            }
            Some(format!("{}\n", s))
        } else {
            Some(format!(
                "═══════════════════════════════════════\ncargo build: {} errors, {} warnings ({} crates)\n",
                self.error_count, self.warnings, self.compiled
            ))
        }
    }
}

struct CargoTestHandler {
    in_failure_section: bool,
    in_failure_names: bool,
    summary_lines: Vec<String>,
    has_compile_errors: bool,
}

impl CargoTestHandler {
    fn new() -> Self {
        Self {
            in_failure_section: false,
            in_failure_names: false,
            summary_lines: Vec::new(),
            has_compile_errors: false,
        }
    }
}

impl BlockHandler for CargoTestHandler {
    fn should_skip(&mut self, line: &str) -> bool {
        let trimmed = line.trim_start();
        if trimmed.starts_with("Compiling")
            || trimmed.starts_with("Downloading")
            || trimmed.starts_with("Downloaded")
            || trimmed.starts_with("Finished")
        {
            return true;
        }
        if line.starts_with("running ") {
            return true;
        }
        if line.starts_with("test ") && line.ends_with("... ok") {
            return true;
        }
        // Track compile errors for fallback
        if trimmed.starts_with("error[") || trimmed.starts_with("error:") {
            self.has_compile_errors = true;
        }
        // "failures:" toggles section state
        if line == "failures:" {
            if self.in_failure_section {
                // Second "failures:" = list of failure names — skip them
                self.in_failure_names = true;
            }
            self.in_failure_section = true;
            return true;
        }
        // Skip the failure name listing section
        if self.in_failure_names {
            if line.starts_with("test result:") {
                self.in_failure_names = false;
                self.in_failure_section = false;
                self.summary_lines.push(line.to_string());
                return true;
            }
            return true;
        }
        if line.starts_with("test result:") {
            self.summary_lines.push(line.to_string());
            self.in_failure_section = false;
            return true;
        }
        false
    }

    fn is_block_start(&mut self, line: &str) -> bool {
        self.in_failure_section && line.starts_with("---- ")
    }

    fn is_block_continuation(&mut self, line: &str, _block: &[String]) -> bool {
        self.in_failure_section && !line.starts_with("---- ")
    }

    fn format_summary(&self, _exit_code: i32, raw: &str) -> Option<String> {
        if self.summary_lines.is_empty() && self.has_compile_errors {
            let build_filtered = filter_cargo_build(raw);
            if build_filtered.starts_with("cargo build:") {
                return Some(format!(
                    "{}\n",
                    build_filtered.replacen("cargo build:", "cargo test:", 1)
                ));
            }
            // Fallback: last 5 meaningful lines
            let meaningful: Vec<&str> = raw
                .lines()
                .filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with("Compiling"))
                .collect();
            let last5: Vec<&str> = meaningful.iter().rev().take(5).rev().copied().collect();
            return Some(format!("{}\n", last5.join("\n")));
        }

        // No failures emitted — aggregate pass results
        let mut aggregated: Option<AggregatedTestResult> = None;
        let mut all_parsed = true;

        for line in &self.summary_lines {
            if let Some(parsed) = AggregatedTestResult::parse_line(line) {
                if let Some(ref mut agg) = aggregated {
                    agg.merge(&parsed);
                } else {
                    aggregated = Some(parsed);
                }
            } else {
                all_parsed = false;
                break;
            }
        }

        if all_parsed {
            if let Some(agg) = aggregated {
                if agg.suites > 0 {
                    return Some(format!("{}\n", agg.format_compact()));
                }
            }
        }

        // Fallback: show raw summary lines
        if !self.summary_lines.is_empty() {
            let mut s = String::new();
            for line in &self.summary_lines {
                s.push_str(line);
                s.push('\n');
            }
            return Some(s);
        }

        None
    }
}

/// Generic cargo command runner with filtering.
/// Builds the Command with restored `--` separator, then delegates to shared runner.
fn run_cargo_filtered<F>(
    subcommand: &str,
    args: &[String],
    verbose: u8,
    filter_fn: F,
) -> Result<i32>
where
    F: Fn(&str) -> String,
{
    let mut cmd = resolved_command("cargo");
    cmd.arg(subcommand);

    let restored_args = restore_double_dash(args);
    for arg in &restored_args {
        cmd.arg(arg);
    }

    if verbose > 0 {
        eprintln!("Running: cargo {} {}", subcommand, restored_args.join(" "));
    }

    runner::run_filtered(
        cmd,
        &format!("cargo {}", subcommand),
        &restored_args.join(" "),
        filter_fn,
        runner::RunOptions::with_tee(&format!("cargo_{}", subcommand)),
    )
}

fn run_cargo_streamed(
    subcommand: &str,
    args: &[String],
    verbose: u8,
    filter: Box<dyn StreamFilter>,
) -> Result<i32> {
    let mut cmd = resolved_command("cargo");
    cmd.arg(subcommand);

    let restored_args = restore_double_dash(args);
    for arg in &restored_args {
        cmd.arg(arg);
    }

    if verbose > 0 {
        eprintln!("Running: cargo {} {}", subcommand, restored_args.join(" "));
    }

    runner::run_streamed(
        cmd,
        &format!("cargo {}", subcommand),
        &restored_args.join(" "),
        filter,
        runner::RunOptions::with_tee(&format!("cargo_{}", subcommand)),
    )
}

fn run_build(args: &[String], verbose: u8) -> Result<i32> {
    run_cargo_streamed(
        "build",
        args,
        verbose,
        Box::new(BlockStreamFilter::new(CargoBuildHandler::new())),
    )
}

fn run_test(args: &[String], verbose: u8) -> Result<i32> {
    run_cargo_streamed(
        "test",
        args,
        verbose,
        Box::new(BlockStreamFilter::new(CargoTestHandler::new())),
    )
}

fn run_clippy(args: &[String], verbose: u8) -> Result<i32> {
    run_cargo_filtered("clippy", args, verbose, filter_cargo_clippy)
}

fn run_check(args: &[String], verbose: u8) -> Result<i32> {
    run_cargo_streamed(
        "check",
        args,
        verbose,
        Box::new(BlockStreamFilter::new(CargoBuildHandler::new())),
    )
}

fn run_install(args: &[String], verbose: u8) -> Result<i32> {
    run_cargo_filtered("install", args, verbose, filter_cargo_install)
}

fn run_nextest(args: &[String], verbose: u8) -> Result<i32> {
    run_cargo_filtered("nextest", args, verbose, filter_cargo_nextest)
}

/// Format crate name + version into a display string
fn format_crate_info(name: &str, version: &str, fallback: &str) -> String {
    if name.is_empty() {
        fallback.to_string()
    } else if version.is_empty() {
        name.to_string()
    } else {
        format!("{} {}", name, version)
    }
}

/// Filter cargo install output - strip dep compilation, keep installed/replaced/errors
fn filter_cargo_install(output: &str) -> String {
    let mut errors: Vec<String> = Vec::new();
    let mut error_count = 0;
    let mut compiled = 0;
    let mut in_error = false;
    let mut current_error = Vec::new();
    let mut installed_crate = String::new();
    let mut installed_version = String::new();
    let mut replaced_lines: Vec<String> = Vec::new();
    let mut already_installed = false;
    let mut ignored_line = String::new();

    for line in output.lines() {
        let trimmed = line.trim_start();

        // Strip noise: dep compilation, downloading, locking, etc.
        if trimmed.starts_with("Compiling") {
            compiled += 1;
            continue;
        }
        if trimmed.starts_with("Downloading")
            || trimmed.starts_with("Downloaded")
            || trimmed.starts_with("Locking")
            || trimmed.starts_with("Updating")
            || trimmed.starts_with("Adding")
            || trimmed.starts_with("Finished")
            || trimmed.starts_with("Blocking waiting for file lock")
        {
            continue;
        }

        // Keep: Installing line (extract crate name + version)
        if trimmed.starts_with("Installing") {
            let rest = trimmed.strip_prefix("Installing").unwrap_or("").trim();
            if !rest.is_empty() && !rest.starts_with('/') {
                if let Some((name, version)) = rest.split_once(' ') {
                    installed_crate = name.to_string();
                    installed_version = version.to_string();
                } else {
                    installed_crate = rest.to_string();
                }
            }
            continue;
        }

        // Keep: Installed line (extract crate + version if not already set)
        if trimmed.starts_with("Installed") {
            let rest = trimmed.strip_prefix("Installed").unwrap_or("").trim();
            if !rest.is_empty() && installed_crate.is_empty() {
                let mut parts = rest.split_whitespace();
                if let (Some(name), Some(version)) = (parts.next(), parts.next()) {
                    installed_crate = name.to_string();
                    installed_version = version.to_string();
                }
            }
            continue;
        }

        // Keep: Replacing/Replaced lines
        if trimmed.starts_with("Replacing") || trimmed.starts_with("Replaced") {
            replaced_lines.push(trimmed.to_string());
            continue;
        }

        // Keep: "Ignored package" (already up to date)
        if trimmed.starts_with("Ignored package") {
            already_installed = true;
            ignored_line = trimmed.to_string();
            continue;
        }

        // Keep: actionable warnings (e.g., "be sure to add `/path` to your PATH")
        // Skip summary lines like "warning: `crate` generated N warnings"
        if line.starts_with("warning:") {
            if !(line.contains("generated") && line.contains("warning")) {
                replaced_lines.push(line.to_string());
            }
            continue;
        }

        // Detect error blocks
        if line.starts_with("error[") || line.starts_with("error:") {
            if line.contains("aborting due to") || line.contains("could not compile") {
                continue;
            }
            if in_error && !current_error.is_empty() {
                errors.push(current_error.join("\n"));
                current_error.clear();
            }
            error_count += 1;
            in_error = true;
            current_error.push(line.to_string());
        } else if in_error {
            if line.trim().is_empty() && current_error.len() > 3 {
                errors.push(current_error.join("\n"));
                current_error.clear();
                in_error = false;
            } else {
                current_error.push(line.to_string());
            }
        }
    }

    if !current_error.is_empty() {
        errors.push(current_error.join("\n"));
    }

    // Already installed / up to date
    if already_installed {
        let info = ignored_line.split('`').nth(1).unwrap_or(&ignored_line);
        return format!("cargo install: {} already installed", info);
    }

    // Errors
    if error_count > 0 {
        let crate_info = format_crate_info(&installed_crate, &installed_version, "");
        let deps_info = if compiled > 0 {
            format!(", {} deps compiled", compiled)
        } else {
            String::new()
        };

        let mut result = String::new();
        if crate_info.is_empty() {
            result.push_str(&format!(
                "cargo install: {} error{}{}\n",
                error_count,
                if error_count > 1 { "s" } else { "" },
                deps_info
            ));
        } else {
            result.push_str(&format!(
                "cargo install: {} error{} ({}{})\n",
                error_count,
                if error_count > 1 { "s" } else { "" },
                crate_info,
                deps_info
            ));
        }
        result.push_str("═══════════════════════════════════════\n");

        const MAX_INSTALL_ERRORS: usize = CAP_ERRORS;
        for (i, err) in errors.iter().enumerate().take(MAX_INSTALL_ERRORS) {
            result.push_str(err);
            result.push('\n');
            if i < errors.len() - 1 {
                result.push('\n');
            }
        }

        if errors.len() > MAX_INSTALL_ERRORS {
            result.push_str(&format!(
                "\n… +{} more issues\n",
                errors.len() - MAX_INSTALL_ERRORS
            ));
            let all_errors = errors.join("\n\n");
            if let Some(hint) = crate::core::tee::force_tee_hint(&all_errors, "cargo-build-errors")
            {
                result.push_str(&format!("  {}\n", hint));
            }
        }

        return result.trim().to_string();
    }

    // Success
    let crate_info = format_crate_info(&installed_crate, &installed_version, "package");

    let mut result = format!("cargo install ({}, {} deps compiled)", crate_info, compiled);

    for line in &replaced_lines {
        result.push_str(&format!("\n  {}", line));
    }

    result
}

/// Push a completed failure block (header + body) into the failures list, then clear the buffers.
fn flush_failure_block(header: &mut String, body: &mut Vec<String>, failures: &mut Vec<String>) {
    if header.is_empty() {
        return;
    }
    let mut block = header.clone();
    if !body.is_empty() {
        block.push('\n');
        block.push_str(&body.join("\n"));
    }
    failures.push(block);
    header.clear();
    body.clear();
}

/// Filter cargo nextest output - show failures + compact summary
fn filter_cargo_nextest(output: &str) -> String {
    static SUMMARY_RE: OnceLock<regex::Regex> = OnceLock::new();
    let summary_re = SUMMARY_RE.get_or_init(|| {
        regex::Regex::new(
            r"Summary \[\s*([\d.]+)s\]\s+(\d+) tests? run:\s+(\d+) passed(?:,\s+(\d+) failed)?(?:,\s+(\d+) skipped)?"
        ).expect("invalid nextest summary regex")
    });

    static STARTING_RE: OnceLock<regex::Regex> = OnceLock::new();
    let starting_re = STARTING_RE.get_or_init(|| {
        regex::Regex::new(r"Starting \d+ tests? across (\d+) binar(?:y|ies)")
            .expect("invalid nextest starting regex")
    });

    let mut failures: Vec<String> = Vec::new();
    let mut in_failure_block = false;
    let mut past_summary = false;
    let mut current_failure_header = String::new();
    let mut current_failure_body = Vec::new();
    let mut summary_line = String::new();
    let mut binaries: u32 = 0;
    let mut has_cancel_line = false;

    for line in output.lines() {
        let trimmed = line.trim();

        // Strip compilation noise
        if trimmed.starts_with("Compiling")
            || trimmed.starts_with("Downloading")
            || trimmed.starts_with("Downloaded")
            || trimmed.starts_with("Finished")
            || trimmed.starts_with("Locking")
            || trimmed.starts_with("Updating")
        {
            continue;
        }

        // Strip separator lines (────)
        if trimmed.starts_with("────") {
            continue;
        }

        // Skip post-summary recap lines (FAIL duplicates + "error: test run failed")
        if past_summary {
            continue;
        }

        // Parse binary count from Starting line
        if trimmed.starts_with("Starting") {
            if let Some(caps) = starting_re.captures(trimmed) {
                if let Some(m) = caps.get(1) {
                    binaries = m.as_str().parse().unwrap_or(0);
                }
            }
            continue;
        }

        // Strip PASS lines
        if trimmed.starts_with("PASS") {
            if in_failure_block {
                flush_failure_block(
                    &mut current_failure_header,
                    &mut current_failure_body,
                    &mut failures,
                );
                in_failure_block = false;
            }
            continue;
        }

        // Detect FAIL lines
        if trimmed.starts_with("FAIL") {
            // Close previous failure block if any
            if in_failure_block {
                flush_failure_block(
                    &mut current_failure_header,
                    &mut current_failure_body,
                    &mut failures,
                );
            }
            current_failure_header = trimmed.to_string();
            in_failure_block = true;
            continue;
        }

        // Cancellation notice
        if trimmed.starts_with("Cancelling") || trimmed.starts_with("Canceling") {
            has_cancel_line = true;
            continue;
        }

        // Nextest run ID line
        if trimmed.starts_with("Nextest run ID") {
            continue;
        }

        // Parse summary
        if trimmed.starts_with("Summary") {
            summary_line = trimmed.to_string();
            if in_failure_block {
                flush_failure_block(
                    &mut current_failure_header,
                    &mut current_failure_body,
                    &mut failures,
                );
                in_failure_block = false;
            }
            past_summary = true;
            continue;
        }

        // Collect failure body lines (stdout/stderr sections)
        if in_failure_block {
            current_failure_body.push(line.to_string());
        }
    }

    // Close last failure block
    if in_failure_block {
        flush_failure_block(
            &mut current_failure_header,
            &mut current_failure_body,
            &mut failures,
        );
    }

    // Parse summary with regex
    if let Some(caps) = summary_re.captures(&summary_line) {
        let duration = caps.get(1).map_or("?", |m| m.as_str());
        let passed: u32 = caps
            .get(3)
            .and_then(|m| m.as_str().parse().ok())
            .unwrap_or(0);
        let failed: u32 = caps
            .get(4)
            .and_then(|m| m.as_str().parse().ok())
            .unwrap_or(0);
        let skipped: u32 = caps
            .get(5)
            .and_then(|m| m.as_str().parse().ok())
            .unwrap_or(0);

        let binary_text = match binaries.cmp(&1) {
            Ordering::Greater => format!("{} binaries", binaries),
            Ordering::Equal => "1 binary".to_string(),
            Ordering::Less => String::new(),
        };

        if failed == 0 {
            // All pass - compact single line
            let mut parts = vec![format!("{} passed", passed)];
            if skipped > 0 {
                parts.push(format!("{} skipped", skipped));
            }
            let meta = if binary_text.is_empty() {
                format!("{}s", duration)
            } else {
                format!("{}, {}s", binary_text, duration)
            };
            return format!("cargo nextest: {} ({})", parts.join(", "), meta);
        }

        // With failures - show failure details then summary
        let mut result = String::new();

        for failure in &failures {
            result.push_str(failure);
            result.push('\n');
        }

        if has_cancel_line {
            result.push_str("Cancelling due to test failure\n");
        }

        let mut summary_parts = vec![format!("{} passed", passed)];
        if failed > 0 {
            summary_parts.push(format!("{} failed", failed));
        }
        if skipped > 0 {
            summary_parts.push(format!("{} skipped", skipped));
        }
        let meta = if binary_text.is_empty() {
            format!("{}s", duration)
        } else {
            format!("{}, {}s", binary_text, duration)
        };
        result.push_str(&format!(
            "cargo nextest: {} ({})",
            summary_parts.join(", "),
            meta
        ));

        return result.trim().to_string();
    }

    // Fallback: if summary regex didn't match, show what we have
    if !failures.is_empty() {
        let mut result = String::new();
        for failure in &failures {
            result.push_str(failure);
            result.push('\n');
        }
        if !summary_line.is_empty() {
            result.push_str(&summary_line);
        }
        return result.trim().to_string();
    }

    if !summary_line.is_empty() {
        return summary_line;
    }

    // Empty or unrecognized
    String::new()
}

fn filter_cargo_build(output: &str) -> String {
    let mut handler = CargoBuildHandler::new();
    let mut blocks: Vec<Vec<String>> = Vec::new();
    let mut current_block: Vec<String> = Vec::new();
    let mut in_block = false;

    for line in output.lines() {
        if handler.should_skip(line) {
            continue;
        }
        if handler.is_block_start(line) {
            if in_block && !current_block.is_empty() {
                blocks.push(std::mem::take(&mut current_block));
            }
            in_block = true;
            current_block.push(line.to_string());
        } else if in_block {
            if handler.is_block_continuation(line, &current_block) {
                current_block.push(line.to_string());
            } else {
                blocks.push(std::mem::take(&mut current_block));
                in_block = false;
            }
        }
    }
    if !current_block.is_empty() {
        blocks.push(current_block);
    }

    if handler.error_count == 0 && handler.warnings == 0 {
        let mut s = format!("cargo build ({} crates compiled)", handler.compiled);
        if let Some(ref finished) = handler.finished_line {
            s = format!("{}\n{}", s, finished);
        }
        return s;
    }

    let mut result = format!(
        "cargo build: {} errors, {} warnings ({} crates)\n═══════════════════════════════════════\n",
        handler.error_count, handler.warnings, handler.compiled
    );
    const MAX_CHECK_BLOCKS: usize = CAP_ERRORS;
    for (i, blk) in blocks.iter().enumerate().take(MAX_CHECK_BLOCKS) {
        result.push_str(&blk.join("\n"));
        result.push('\n');
        if i < blocks.len() - 1 {
            result.push('\n');
        }
    }
    if blocks.len() > MAX_CHECK_BLOCKS {
        result.push_str(&format!(
            "\n… +{} more issues\n",
            blocks.len() - MAX_CHECK_BLOCKS
        ));
        let all_blocks: String = blocks
            .iter()
            .map(|b| b.join("\n"))
            .collect::<Vec<_>>()
            .join("\n\n");
        if let Some(hint) = crate::core::tee::force_tee_hint(&all_blocks, "cargo-check-issues") {
            result.push_str(&format!("  {}\n", hint));
        }
    }
    result.trim().to_string()
}

/// Aggregated test results for compact display
#[derive(Debug, Default, Clone)]
struct AggregatedTestResult {
    passed: usize,
    failed: usize,
    ignored: usize,
    measured: usize,
    filtered_out: usize,
    suites: usize,
    duration_secs: f64,
    has_duration: bool,
}

impl AggregatedTestResult {
    /// Parse a test result summary line
    /// Format: "test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s"
    fn parse_line(line: &str) -> Option<Self> {
        static RE: OnceLock<regex::Regex> = OnceLock::new();
        let re = RE.get_or_init(|| {
            regex::Regex::new(
                r"test result: (\w+)\.\s+(\d+) passed;\s+(\d+) failed;\s+(\d+) ignored;\s+(\d+) measured;\s+(\d+) filtered out(?:;\s+finished in ([\d.]+)s)?"
            ).unwrap()
        });

        let caps = re.captures(line)?;
        let status = caps.get(1)?.as_str();

        // Only aggregate if status is "ok" (all tests passed)
        if status != "ok" {
            return None;
        }

        let passed = caps.get(2)?.as_str().parse().ok()?;
        let failed = caps.get(3)?.as_str().parse().ok()?;
        let ignored = caps.get(4)?.as_str().parse().ok()?;
        let measured = caps.get(5)?.as_str().parse().ok()?;
        let filtered_out = caps.get(6)?.as_str().parse().ok()?;

        let (duration_secs, has_duration) = if let Some(duration_match) = caps.get(7) {
            (duration_match.as_str().parse().unwrap_or(0.0), true)
        } else {
            (0.0, false)
        };

        Some(Self {
            passed,
            failed,
            ignored,
            measured,
            filtered_out,
            suites: 1,
            duration_secs,
            has_duration,
        })
    }

    /// Merge another test result into this one
    fn merge(&mut self, other: &Self) {
        self.passed += other.passed;
        self.failed += other.failed;
        self.ignored += other.ignored;
        self.measured += other.measured;
        self.filtered_out += other.filtered_out;
        self.suites += other.suites;
        self.duration_secs += other.duration_secs;
        self.has_duration = self.has_duration && other.has_duration;
    }

    /// Format as compact single line
    fn format_compact(&self) -> String {
        let mut parts = vec![format!("{} passed", self.passed)];

        if self.ignored > 0 {
            parts.push(format!("{} ignored", self.ignored));
        }
        if self.filtered_out > 0 {
            parts.push(format!("{} filtered out", self.filtered_out));
        }

        let counts = parts.join(", ");

        let suite_text = if self.suites == 1 {
            "1 suite".to_string()
        } else {
            format!("{} suites", self.suites)
        };

        if self.has_duration {
            format!(
                "cargo test: {} ({}, {:.2}s)",
                counts, suite_text, self.duration_secs
            )
        } else {
            format!("cargo test: {} ({})", counts, suite_text)
        }
    }
}

pub(crate) fn filter_cargo_test(output: &str) -> String {
    let mut failures: Vec<String> = Vec::new();
    let mut summary_lines: Vec<String> = Vec::new();
    let mut in_failure_section = false;
    let mut current_failure = Vec::new();

    for line in output.lines() {
        // Skip compilation lines
        if line.trim_start().starts_with("Compiling")
            || line.trim_start().starts_with("Downloading")
            || line.trim_start().starts_with("Downloaded")
            || line.trim_start().starts_with("Finished")
        {
            continue;
        }

        // Skip "running N tests" and individual "test ... ok" lines
        if line.starts_with("running ") || (line.starts_with("test ") && line.ends_with("... ok")) {
            continue;
        }

        // Detect failures section
        if line == "failures:" {
            in_failure_section = true;
            continue;
        }

        if in_failure_section {
            if line.starts_with("test result:") {
                in_failure_section = false;
                summary_lines.push(line.to_string());
            } else if line.starts_with("    ") || line.starts_with("---- ") {
                current_failure.push(line.to_string());
            } else if line.trim().is_empty() && !current_failure.is_empty() {
                failures.push(current_failure.join("\n"));
                current_failure.clear();
            } else if !line.trim().is_empty() {
                current_failure.push(line.to_string());
            }
        }

        // Capture test result summary
        if !in_failure_section && line.starts_with("test result:") {
            summary_lines.push(line.to_string());
        }
    }

    if !current_failure.is_empty() {
        failures.push(current_failure.join("\n"));
    }

    let mut result = String::new();

    if failures.is_empty() && !summary_lines.is_empty() {
        // All passed - try to aggregate
        let mut aggregated: Option<AggregatedTestResult> = None;
        let mut all_parsed = true;

        for line in &summary_lines {
            if let Some(parsed) = AggregatedTestResult::parse_line(line) {
                if let Some(ref mut agg) = aggregated {
                    agg.merge(&parsed);
                } else {
                    aggregated = Some(parsed);
                }
            } else {
                all_parsed = false;
                break;
            }
        }

        // If all lines parsed successfully and we have at least one suite, return compact format
        if all_parsed {
            if let Some(agg) = aggregated {
                if agg.suites > 0 {
                    return agg.format_compact();
                }
            }
        }

        // Fallback: use original behavior if regex failed
        for line in &summary_lines {
            result.push_str(&format!("{}\n", line));
        }
        return result.trim().to_string();
    }

    if !failures.is_empty() {
        result.push_str(&format!("FAILURES ({}):\n", failures.len()));
        result.push_str("═══════════════════════════════════════\n");
        const MAX_FAILURES: usize = CAP_WARNINGS;
        for (i, failure) in failures.iter().enumerate().take(MAX_FAILURES) {
            result.push_str(&format!("{}. {}\n", i + 1, truncate(failure, 200)));
        }
        if failures.len() > MAX_FAILURES {
            result.push_str(&format!("\n… +{} more failures\n", failures.len() - MAX_FAILURES));
            let all_failures = failures.join("\n\n");
            if let Some(hint) =
                crate::core::tee::force_tee_hint(&all_failures, "cargo-test-failures")
            {
                result.push_str(&format!("  {}\n", hint));
            }
        }
        result.push('\n');
    }

    for line in &summary_lines {
        result.push_str(&format!("{}\n", line));
    }

    if result.trim().is_empty() {
        let has_compile_errors = output.lines().any(|line| {
            let trimmed = line.trim_start();
            trimmed.starts_with("error[") || trimmed.starts_with("error:")
        });

        if has_compile_errors {
            let build_filtered = filter_cargo_build(output);
            if build_filtered.starts_with("cargo build:") {
                return build_filtered.replacen("cargo build:", "cargo test:", 1);
            }
        }

        // Fallback: show last meaningful lines
        let meaningful: Vec<&str> = output
            .lines()
            .filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with("Compiling"))
            .collect();
        for line in meaningful.iter().rev().take(5).rev() {
            result.push_str(&format!("{}\n", line));
        }
    }

    result.trim().to_string()
}

/// Filter cargo clippy output - show full error blocks, group warnings by lint rule
fn filter_cargo_clippy(output: &str) -> String {
    let mut by_rule: HashMap<String, Vec<String>> = HashMap::new();
    let mut error_count = 0;
    let mut warning_count = 0;
    // Each entry is a full multi-line error block (headline + location + code context)
    let mut error_blocks: Vec<Vec<String>> = Vec::new();

    let mut current_rule = String::new();
    let mut in_error = false;
    let mut current_block: Vec<String> = Vec::new();

    for line in output.lines() {
        // Skip compilation progress lines
        if line.trim_start().starts_with("Compiling")
            || line.trim_start().starts_with("Checking")
            || line.trim_start().starts_with("Downloading")
            || line.trim_start().starts_with("Downloaded")
            || line.trim_start().starts_with("Finished")
        {
            if in_error && !current_block.is_empty() {
                error_blocks.push(current_block.clone());
                current_block.clear();
                in_error = false;
            }
            continue;
        }

        // Skip noise: summary counts and abort lines
        if (line.contains("generated") && line.contains("warning"))
            || line.contains("aborting due to")
            || line.contains("could not compile")
        {
            continue;
        }

        let is_error_line = line.starts_with("error:") || line.starts_with("error[");
        let is_warning_line = line.starts_with("warning:") || line.starts_with("warning[");

        if is_error_line || is_warning_line {
            // Flush any in-progress error block before starting a new diagnostic
            if in_error && !current_block.is_empty() {
                error_blocks.push(current_block.clone());
                current_block.clear();
            }
            in_error = false;

            if is_error_line {
                error_count += 1;
                in_error = true;
                current_block.push(line.to_string());
            } else {
                warning_count += 1;
            }

            // Extract rule/error-code from brackets for warning grouping
            current_rule = if let Some(bracket_start) = line.rfind('[') {
                if let Some(bracket_end) = line.rfind(']') {
                    line[bracket_start + 1..bracket_end].to_string()
                } else {
                    line.to_string()
                }
            } else {
                let prefix = if is_error_line {
                    "error: "
                } else {
                    "warning: "
                };
                line.strip_prefix(prefix).unwrap_or(line).to_string()
            };
        } else if line.trim_start().starts_with("--> ") {
            let location = line.trim_start().trim_start_matches("--> ").to_string();
            if !current_rule.is_empty() {
                by_rule
                    .entry(current_rule.clone())
                    .or_default()
                    .push(location);
            }
            if in_error {
                current_block.push(line.to_string());
            }
        } else if in_error {
            if line.trim().is_empty() {
                // Blank line terminates the error block
                if !current_block.is_empty() {
                    error_blocks.push(current_block.clone());
                    current_block.clear();
                }
                in_error = false;
            } else if current_block.len() < 15 {
                // Collect code-context lines (|, ^, = note:, help:, etc.)
                current_block.push(line.to_string());
            }
        }
    }

    // Flush final error block
    if in_error && !current_block.is_empty() {
        error_blocks.push(current_block);
    }

    if error_count == 0 && warning_count == 0 {
        return "cargo clippy: No issues found".to_string();
    }

    let mut result = String::new();
    result.push_str(&format!(
        "cargo clippy: {} errors, {} warnings\n",
        error_count, warning_count
    ));
    result.push_str("═══════════════════════════════════════\n");

    // Show full error blocks so developers can see what needs fixing
    if !error_blocks.is_empty() {
        const MAX_CLIPPY_ERRORS: usize = CAP_WARNINGS;
        result.push_str("\nErrors:\n");
        for block in error_blocks.iter().take(MAX_CLIPPY_ERRORS) {
            for block_line in block {
                result.push_str(&format!("  {}\n", truncate(block_line, 160)));
            }
            result.push('\n');
        }
        if error_blocks.len() > MAX_CLIPPY_ERRORS {
            result.push_str(&format!(
                "  … +{} more errors\n",
                error_blocks.len() - MAX_CLIPPY_ERRORS
            ));
            let all_blocks: String = error_blocks
                .iter()
                .map(|b| b.join("\n"))
                .collect::<Vec<_>>()
                .join("\n\n");
            if let Some(hint) =
                crate::core::tee::force_tee_hint(&all_blocks, "cargo-clippy-errors")
            {
                result.push_str(&format!("  {}\n", hint));
            }
        }
    }

    // Sort warning rules by frequency
    let mut rule_counts: Vec<_> = by_rule.iter().collect();
    rule_counts.sort_by_key(|b| std::cmp::Reverse(b.1.len()));

    const MAX_RULES: usize = CAP_LIST;
    for (rule, locations) in rule_counts.iter().take(MAX_RULES) {
        result.push_str(&format!("  {} ({}x)\n", rule, locations.len()));
        for loc in locations.iter().take(3) {
            result.push_str(&format!("    {}\n", loc));
        }
        if locations.len() > 3 {
            result.push_str(&format!("    … +{} more\n", locations.len() - 3));
        }
    }

    if by_rule.len() > MAX_RULES {
        result.push_str(&format!("\n… +{} more rules\n", by_rule.len() - MAX_RULES));
        let all_rules = rule_counts
            .iter()
            .map(|(rule, locs)| format!("{} ({}x)", rule, locs.len()))
            .collect::<Vec<_>>()
            .join("\n");
        if let Some(hint) =
            crate::core::tee::force_tee_tail_hint(&all_rules, "cargo-clippy-rules", MAX_RULES + 1)
        {
            result.push_str(&format!("  {}\n", hint));
        }
    }

    result.trim().to_string()
}

pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<i32> {
    crate::core::runner::run_passthrough("cargo", args, verbose)
}

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

    #[test]
    fn test_restore_double_dash_with_separator() {
        // rtk cargo test -- --nocapture → clap gives ["--nocapture"]
        let args: Vec<String> = vec!["--nocapture".into()];
        let raw = vec![
            "rtk".into(),
            "cargo".into(),
            "test".into(),
            "--".into(),
            "--nocapture".into(),
        ];
        let result = restore_double_dash_with_raw(&args, &raw);
        assert_eq!(result, vec!["--", "--nocapture"]);
    }

    #[test]
    fn test_restore_double_dash_with_test_name() {
        // rtk cargo test my_test -- --nocapture → clap gives ["my_test", "--nocapture"]
        let args: Vec<String> = vec!["my_test".into(), "--nocapture".into()];
        let raw = vec![
            "rtk".into(),
            "cargo".into(),
            "test".into(),
            "my_test".into(),
            "--".into(),
            "--nocapture".into(),
        ];
        let result = restore_double_dash_with_raw(&args, &raw);
        assert_eq!(result, vec!["my_test", "--", "--nocapture"]);
    }

    #[test]
    fn test_restore_double_dash_without_separator() {
        // rtk cargo test my_test → no --, args unchanged
        let args: Vec<String> = vec!["my_test".into()];
        let raw = vec![
            "rtk".into(),
            "cargo".into(),
            "test".into(),
            "my_test".into(),
        ];
        let result = restore_double_dash_with_raw(&args, &raw);
        assert_eq!(result, vec!["my_test"]);
    }

    #[test]
    fn test_restore_double_dash_empty_args() {
        let args: Vec<String> = vec![];
        let raw = vec!["rtk".into(), "cargo".into(), "test".into()];
        let result = restore_double_dash_with_raw(&args, &raw);
        assert!(result.is_empty());
    }

    #[test]
    fn test_restore_double_dash_clippy() {
        // rtk cargo clippy -- -D warnings → clap gives ["-D", "warnings"]
        let args: Vec<String> = vec!["-D".into(), "warnings".into()];
        let raw = vec![
            "rtk".into(),
            "cargo".into(),
            "clippy".into(),
            "--".into(),
            "-D".into(),
            "warnings".into(),
        ];
        let result = restore_double_dash_with_raw(&args, &raw);
        assert_eq!(result, vec!["--", "-D", "warnings"]);
    }

    #[test]
    fn test_restore_double_dash_clippy_with_package_flags() {
        // rtk cargo clippy -p my-service -p my-crate -- -D warnings
        // Clap with trailing_var_arg preserves "--" when args precede it
        // → clap gives ["-p", "my-service", "-p", "my-crate", "--", "-D", "warnings"]
        let args: Vec<String> = vec![
            "-p".into(),
            "my-service".into(),
            "-p".into(),
            "my-crate".into(),
            "--".into(),
            "-D".into(),
            "warnings".into(),
        ];
        let raw = vec![
            "rtk".into(),
            "cargo".into(),
            "clippy".into(),
            "-p".into(),
            "my-service".into(),
            "-p".into(),
            "my-crate".into(),
            "--".into(),
            "-D".into(),
            "warnings".into(),
        ];
        let result = restore_double_dash_with_raw(&args, &raw);
        // Should NOT double the "--"
        assert_eq!(
            result,
            vec!["-p", "my-service", "-p", "my-crate", "--", "-D", "warnings"]
        );
        // Verify only one "--" exists
        assert_eq!(result.iter().filter(|a| *a == "--").count(), 1);
    }

    #[test]
    fn test_filter_cargo_build_success() {
        let output = r#"   Compiling libc v0.2.153
   Compiling cfg-if v1.0.0
   Compiling rtk v0.5.0
    Finished dev [unoptimized + debuginfo] target(s) in 15.23s
"#;
        let result = filter_cargo_build(output);
        assert!(result.contains("cargo build"));
        assert!(result.contains("3 crates compiled"));
    }

    #[test]
    fn test_filter_cargo_build_errors() {
        let output = r#"   Compiling rtk v0.5.0
error[E0308]: mismatched types
 --> src/main.rs:10:5
  |
10|     "hello"
  |     ^^^^^^^ expected `i32`, found `&str`

error: aborting due to 1 previous error
"#;
        let result = filter_cargo_build(output);
        assert!(result.contains("1 errors"));
        assert!(result.contains("E0308"));
        assert!(result.contains("mismatched types"));
    }

    #[test]
    fn test_filter_cargo_test_all_pass() {
        let output = r#"   Compiling rtk v0.5.0
    Finished test [unoptimized + debuginfo] target(s) in 2.53s
     Running target/debug/deps/rtk-abc123

running 15 tests
test utils::tests::test_truncate_short_string ... ok
test utils::tests::test_truncate_long_string ... ok
test utils::tests::test_strip_ansi_simple ... ok

test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
"#;
        let result = filter_cargo_test(output);
        assert!(
            result.contains("cargo test: 15 passed (1 suite, 0.01s)"),
            "Expected compact format, got: {}",
            result
        );
        assert!(!result.contains("Compiling"));
        assert!(!result.contains("test utils"));
    }

    #[test]
    fn test_filter_cargo_test_failures() {
        let output = r#"running 5 tests
test foo::test_a ... ok
test foo::test_b ... FAILED
test foo::test_c ... ok

failures:

---- foo::test_b stdout ----
thread 'foo::test_b' panicked at 'assert_eq!(1, 2)'

failures:
    foo::test_b

test result: FAILED. 4 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
"#;
        let result = filter_cargo_test(output);
        assert!(result.contains("FAILURES"));
        assert!(result.contains("test_b"));
        assert!(result.contains("test result:"));
    }

    #[test]
    fn test_filter_cargo_test_multi_suite_all_pass() {
        let output = r#"   Compiling rtk v0.5.0
    Finished test [unoptimized + debuginfo] target(s) in 2.53s
     Running unittests src/lib.rs (target/debug/deps/rtk-abc123)

running 50 tests
test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s

     Running unittests src/main.rs (target/debug/deps/rtk-def456)

running 30 tests
test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s

     Running tests/integration.rs (target/debug/deps/integration-ghi789)

running 25 tests
test result: ok. 25 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s

   Doc-tests rtk

running 32 tests
test result: ok. 32 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s
"#;
        let result = filter_cargo_test(output);
        assert!(
            result.contains("cargo test: 137 passed (4 suites, 1.45s)"),
            "Expected aggregated format, got: {}",
            result
        );
        assert!(!result.contains("running"));
    }

    #[test]
    fn test_filter_cargo_test_multi_suite_with_failures() {
        let output = r#"     Running unittests src/lib.rs

running 20 tests
test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

     Running unittests src/main.rs

running 15 tests
test foo::test_bad ... FAILED

failures:

---- foo::test_bad stdout ----
thread panicked at 'assertion failed'

test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s

     Running tests/integration.rs

running 10 tests
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
"#;
        let result = filter_cargo_test(output);
        // Should NOT aggregate when there are failures
        assert!(result.contains("FAILURES"), "got: {}", result);
        assert!(result.contains("test_bad"), "got: {}", result);
        assert!(result.contains("test result:"), "got: {}", result);
        // Should show individual summaries
        assert!(result.contains("20 passed"), "got: {}", result);
        assert!(result.contains("14 passed"), "got: {}", result);
        assert!(result.contains("10 passed"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_test_all_suites_zero_tests() {
        let output = r#"     Running unittests src/empty1.rs

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/empty2.rs

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/empty3.rs

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
"#;
        let result = filter_cargo_test(output);
        assert!(
            result.contains("cargo test: 0 passed (3 suites, 0.00s)"),
            "Expected compact format for zero tests, got: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_test_with_ignored_and_filtered() {
        let output = r#"     Running unittests src/lib.rs

running 50 tests
test result: ok. 45 passed; 0 failed; 3 ignored; 0 measured; 2 filtered out; finished in 0.50s

     Running tests/integration.rs

running 20 tests
test result: ok. 18 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.20s
"#;
        let result = filter_cargo_test(output);
        assert!(
            result.contains("cargo test: 63 passed, 5 ignored, 2 filtered out (2 suites, 0.70s)"),
            "Expected compact format with ignored and filtered, got: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_test_single_suite_compact() {
        let output = r#"     Running unittests src/main.rs

running 15 tests
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
"#;
        let result = filter_cargo_test(output);
        assert!(
            result.contains("cargo test: 15 passed (1 suite, 0.01s)"),
            "Expected singular 'suite', got: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_test_regex_fallback() {
        let output = r#"     Running unittests src/main.rs

running 15 tests
test result: MALFORMED LINE WITHOUT PROPER FORMAT
"#;
        let result = filter_cargo_test(output);
        // Should fallback to original behavior (show line without checkmark)
        assert!(
            result.contains("test result: MALFORMED"),
            "Expected fallback format, got: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_test_compile_error_preserves_error_header() {
        let output = r#"   Compiling rtk v0.31.0 (/workspace/projects/rtk)
error[E0425]: cannot find value `missing_symbol` in this scope
 --> tests/repro_compile_fail.rs:3:13
  |
3 |     let _ = missing_symbol;
  |             ^^^^^^^^^^^^^^ not found in this scope

For more information about this error, try `rustc --explain E0425`.
error: could not compile `rtk` (test "repro_compile_fail") due to 1 previous error
"#;
        let result = filter_cargo_test(output);
        assert!(result.contains("cargo test: 1 errors, 0 warnings (1 crates)"));
        assert!(result.contains("error[E0425]"), "got: {}", result);
        assert!(
            result.contains("--> tests/repro_compile_fail.rs:3:13"),
            "got: {}",
            result
        );
        assert!(!result.starts_with('|'), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_clippy_clean() {
        let output = r#"    Checking rtk v0.5.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.53s
"#;
        let result = filter_cargo_clippy(output);
        assert!(result.contains("cargo clippy: No issues found"));
    }

    #[test]
    fn test_filter_cargo_clippy_warnings() {
        let output = r#"    Checking rtk v0.5.0
warning: unused variable: `x` [unused_variables]
 --> src/main.rs:10:9
  |
10|     let x = 5;
  |         ^ help: if this is intentional, prefix it with an underscore: `_x`

warning: this function has too many arguments [clippy::too_many_arguments]
 --> src/git.rs:16:1
  |
16| pub fn run(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32, h: i32) {}
  |

warning: `rtk` (bin) generated 2 warnings
    Finished dev [unoptimized + debuginfo] target(s) in 1.53s
"#;
        let result = filter_cargo_clippy(output);
        assert!(result.contains("0 errors, 2 warnings"));
        assert!(result.contains("unused_variables"));
        assert!(result.contains("clippy::too_many_arguments"));
    }

    #[test]
    fn test_filter_cargo_clippy_includes_error_details() {
        let output = r#"    Checking rtk v0.5.0
error: struct literals are not allowed here
warning: unused variable: `x` [unused_variables]
    Finished dev [unoptimized + debuginfo] target(s) in 1.53s
"#;
        let result = filter_cargo_clippy(output);
        assert!(result.contains("cargo clippy: 1 errors, 1 warnings"));
        assert!(result.contains("Errors:"));
        assert!(result.contains("struct literals are not allowed here"));
    }

    #[test]
    fn test_filter_cargo_clippy_shows_full_error_block() {
        // Full multi-line error block must be shown so the developer can debug
        let output = r#"    Checking rtk v0.5.0
error[E0308]: mismatched types
 --> src/main.rs:10:5
  |
9 |     fn foo() -> i32 {
  |                 --- expected `i32` because of return type
10|     "hello"
  |     ^^^^^^^ expected `i32`, found `&str`

error: aborting due to 1 previous error
"#;
        let result = filter_cargo_clippy(output);
        assert!(
            result.contains("cargo clippy: 1 errors, 0 warnings"),
            "got: {}",
            result
        );
        assert!(
            result.contains("error[E0308]: mismatched types"),
            "got: {}",
            result
        );
        assert!(result.contains("src/main.rs:10:5"), "got: {}", result);
        assert!(
            result.contains("expected `i32`, found `&str`"),
            "got: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_clippy_multiple_errors_show_all_blocks() {
        let output = r#"error[E0308]: mismatched types
 --> src/foo.rs:5:3

error[E0425]: cannot find value `x`
 --> src/bar.rs:12:9

error: aborting due to 2 previous errors
"#;
        let result = filter_cargo_clippy(output);
        assert!(result.contains("2 errors"), "got: {}", result);
        assert!(result.contains("src/foo.rs:5:3"), "got: {}", result);
        assert!(result.contains("src/bar.rs:12:9"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_success() {
        let output = r#"  Installing rtk v0.11.0
  Downloading crates ...
  Downloaded anyhow v1.0.80
  Downloaded clap v4.5.0
   Compiling libc v0.2.153
   Compiling cfg-if v1.0.0
   Compiling anyhow v1.0.80
   Compiling clap v4.5.0
   Compiling rtk v0.11.0
    Finished `release` profile [optimized] target(s) in 45.23s
  Replacing /Users/user/.cargo/bin/rtk
   Replaced package `rtk v0.9.4` with `rtk v0.11.0` (/Users/user/.cargo/bin/rtk)
"#;
        let result = filter_cargo_install(output);
        assert!(result.contains("cargo install"), "got: {}", result);
        assert!(result.contains("rtk v0.11.0"), "got: {}", result);
        assert!(result.contains("5 deps compiled"), "got: {}", result);
        assert!(result.contains("Replaced"), "got: {}", result);
        assert!(!result.contains("Compiling"), "got: {}", result);
        assert!(!result.contains("Downloading"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_replace() {
        let output = r#"  Installing rtk v0.11.0
   Compiling rtk v0.11.0
    Finished `release` profile [optimized] target(s) in 10.0s
  Replacing /Users/user/.cargo/bin/rtk
   Replaced package `rtk v0.9.4` with `rtk v0.11.0` (/Users/user/.cargo/bin/rtk)
"#;
        let result = filter_cargo_install(output);
        assert!(result.contains("cargo install"), "got: {}", result);
        assert!(result.contains("Replacing"), "got: {}", result);
        assert!(result.contains("Replaced"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_error() {
        let output = r#"  Installing rtk v0.11.0
   Compiling rtk v0.11.0
error[E0308]: mismatched types
 --> src/main.rs:10:5
  |
10|     "hello"
  |     ^^^^^^^ expected `i32`, found `&str`

error: aborting due to 1 previous error
"#;
        let result = filter_cargo_install(output);
        assert!(result.contains("cargo install: 1 error"), "got: {}", result);
        assert!(result.contains("E0308"), "got: {}", result);
        assert!(result.contains("mismatched types"), "got: {}", result);
        assert!(!result.contains("aborting"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_already_installed() {
        let output = r#"  Ignored package `rtk v0.11.0`, is already installed
"#;
        let result = filter_cargo_install(output);
        assert!(result.contains("already installed"), "got: {}", result);
        assert!(result.contains("rtk v0.11.0"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_up_to_date() {
        let output = r#"  Ignored package `cargo-deb v2.1.0 (/Users/user/cargo-deb)`, is already installed
"#;
        let result = filter_cargo_install(output);
        assert!(result.contains("already installed"), "got: {}", result);
        assert!(result.contains("cargo-deb v2.1.0"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_empty_output() {
        let result = filter_cargo_install("");
        assert!(result.contains("cargo install"), "got: {}", result);
        assert!(result.contains("0 deps compiled"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_path_warning() {
        let output = r#"  Installing rtk v0.11.0
   Compiling rtk v0.11.0
    Finished `release` profile [optimized] target(s) in 10.0s
  Replacing /Users/user/.cargo/bin/rtk
   Replaced package `rtk v0.9.4` with `rtk v0.11.0` (/Users/user/.cargo/bin/rtk)
warning: be sure to add `/Users/user/.cargo/bin` to your PATH
"#;
        let result = filter_cargo_install(output);
        assert!(result.contains("cargo install"), "got: {}", result);
        assert!(
            result.contains("be sure to add"),
            "PATH warning should be kept: {}",
            result
        );
        assert!(result.contains("Replaced"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_multiple_errors() {
        let output = r#"  Installing rtk v0.11.0
   Compiling rtk v0.11.0
error[E0308]: mismatched types
 --> src/main.rs:10:5
  |
10|     "hello"
  |     ^^^^^^^ expected `i32`, found `&str`

error[E0425]: cannot find value `foo`
 --> src/lib.rs:20:9
  |
20|     foo
  |     ^^^ not found in this scope

error: aborting due to 2 previous errors
"#;
        let result = filter_cargo_install(output);
        assert!(
            result.contains("2 errors"),
            "should show 2 errors: {}",
            result
        );
        assert!(result.contains("E0308"), "got: {}", result);
        assert!(result.contains("E0425"), "got: {}", result);
        assert!(!result.contains("aborting"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_locking_and_blocking() {
        let output = r#"  Locking 45 packages to latest compatible versions
  Blocking waiting for file lock on package cache
  Downloading crates ...
  Downloaded serde v1.0.200
   Compiling serde v1.0.200
   Compiling rtk v0.11.0
    Finished `release` profile [optimized] target(s) in 30.0s
  Installing rtk v0.11.0
"#;
        let result = filter_cargo_install(output);
        assert!(result.contains("cargo install"), "got: {}", result);
        assert!(!result.contains("Locking"), "got: {}", result);
        assert!(!result.contains("Blocking"), "got: {}", result);
        assert!(!result.contains("Downloading"), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_install_from_path() {
        let output = r#"  Installing /Users/user/projects/rtk
   Compiling rtk v0.11.0
    Finished `release` profile [optimized] target(s) in 10.0s
"#;
        let result = filter_cargo_install(output);
        // Path-based install: crate info not extracted from path
        assert!(result.contains("cargo install"), "got: {}", result);
        assert!(result.contains("1 deps compiled"), "got: {}", result);
    }

    #[test]
    fn test_format_crate_info() {
        assert_eq!(format_crate_info("rtk", "v0.11.0", ""), "rtk v0.11.0");
        assert_eq!(format_crate_info("rtk", "", ""), "rtk");
        assert_eq!(format_crate_info("", "", "package"), "package");
        assert_eq!(format_crate_info("", "v0.1.0", "fallback"), "fallback");
    }

    #[test]
    fn test_filter_cargo_nextest_all_pass() {
        let output = r#"   Compiling rtk v0.15.2
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s
────────────────────────────
    Starting 301 tests across 1 binary
        PASS [   0.009s] (1/301) rtk::bin/rtk cargo_cmd::tests::test_one
        PASS [   0.008s] (2/301) rtk::bin/rtk cargo_cmd::tests::test_two
        PASS [   0.007s] (301/301) rtk::bin/rtk cargo_cmd::tests::test_last
────────────────────────────
     Summary [   0.192s] 301 tests run: 301 passed, 0 skipped
"#;
        let result = filter_cargo_nextest(output);
        assert_eq!(
            result, "cargo nextest: 301 passed (1 binary, 0.192s)",
            "got: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_nextest_with_failures() {
        let output = r#"    Starting 4 tests across 1 binary (1 test skipped)
        PASS [   0.006s] (1/4) test-proj tests::passing_test
        FAIL [   0.006s] (2/4) test-proj tests::failing_test

  stderr ───

    thread 'tests::failing_test' panicked at src/lib.rs:15:9:
    assertion `left == right` failed
      left: 1
     right: 2

  Cancelling due to test failure: 2 tests still running
        PASS [   0.007s] (3/4) test-proj tests::another_passing
        FAIL [   0.006s] (4/4) test-proj tests::another_failing

  stderr ───

    thread 'tests::another_failing' panicked at src/lib.rs:20:9:
    something went wrong

────────────────────────────
     Summary [   0.007s] 4 tests run: 2 passed, 2 failed, 1 skipped
        FAIL [   0.006s] (2/4) test-proj tests::failing_test
        FAIL [   0.006s] (4/4) test-proj tests::another_failing
error: test run failed
"#;
        let result = filter_cargo_nextest(output);
        assert!(
            result.contains("tests::failing_test"),
            "should contain first failure: {}",
            result
        );
        assert!(
            result.contains("tests::another_failing"),
            "should contain second failure: {}",
            result
        );
        assert!(
            result.contains("panicked"),
            "should contain stderr detail: {}",
            result
        );
        assert!(
            result.contains("2 passed, 2 failed, 1 skipped"),
            "should contain summary: {}",
            result
        );
        assert!(
            !result.contains("PASS"),
            "should not contain PASS lines: {}",
            result
        );
        // Post-summary FAIL recaps must not create duplicate FAIL header entries
        // (test names may appear in both header and stderr body naturally)
        assert_eq!(
            result.matches("FAIL [").count(),
            2,
            "should have exactly 2 FAIL headers (no post-summary duplicates): {}",
            result
        );
        assert!(
            !result.contains("error: test run failed"),
            "should not contain post-summary error line: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_nextest_with_skipped() {
        let output = r#"    Starting 50 tests across 2 binaries (3 tests skipped)
        PASS [   0.010s] (1/50) rtk::bin/rtk test_one
        PASS [   0.010s] (50/50) rtk::bin/rtk test_last
────────────────────────────
     Summary [   0.500s] 50 tests run: 50 passed, 3 skipped
"#;
        let result = filter_cargo_nextest(output);
        assert_eq!(
            result, "cargo nextest: 50 passed, 3 skipped (2 binaries, 0.500s)",
            "got: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_nextest_single_failure_detail() {
        let output = r#"    Starting 2 tests across 1 binary
        PASS [   0.005s] (1/2) proj tests::good
        FAIL [   0.005s] (2/2) proj tests::bad

  stderr ───

    thread 'tests::bad' panicked at src/lib.rs:5:9:
    assertion failed: false

────────────────────────────
     Summary [   0.010s] 2 tests run: 1 passed, 1 failed
        FAIL [   0.005s] (2/2) proj tests::bad
error: test run failed
"#;
        let result = filter_cargo_nextest(output);
        assert!(
            result.contains("assertion failed: false"),
            "should show panic message: {}",
            result
        );
        assert!(
            result.contains("1 passed, 1 failed"),
            "should show summary: {}",
            result
        );
        // Post-summary recap must not duplicate FAIL headers
        assert_eq!(
            result.matches("FAIL [").count(),
            1,
            "should have exactly 1 FAIL header (no post-summary duplicate): {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_nextest_multiple_binaries() {
        let output = r#"    Starting 100 tests across 5 binaries
        PASS [   0.010s] (100/100) test_last
────────────────────────────
     Summary [   1.234s] 100 tests run: 100 passed, 0 skipped
"#;
        let result = filter_cargo_nextest(output);
        assert_eq!(
            result, "cargo nextest: 100 passed (5 binaries, 1.234s)",
            "got: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_nextest_compilation_stripped() {
        let output = r#"   Compiling serde v1.0.200
   Compiling rtk v0.15.2
   Downloading crates ...
    Finished `test` profile [unoptimized + debuginfo] target(s) in 5.00s
────────────────────────────
    Starting 10 tests across 1 binary
        PASS [   0.010s] (10/10) test_last
────────────────────────────
     Summary [   0.050s] 10 tests run: 10 passed, 0 skipped
"#;
        let result = filter_cargo_nextest(output);
        assert!(
            !result.contains("Compiling"),
            "should strip Compiling: {}",
            result
        );
        assert!(
            !result.contains("Downloading"),
            "should strip Downloading: {}",
            result
        );
        assert!(
            !result.contains("Finished"),
            "should strip Finished: {}",
            result
        );
        assert!(
            result.contains("cargo nextest: 10 passed"),
            "got: {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_nextest_empty() {
        let result = filter_cargo_nextest("");
        assert!(result.is_empty(), "got: {}", result);
    }

    #[test]
    fn test_filter_cargo_nextest_cancellation_notice() {
        let output = r#"    Starting 3 tests across 1 binary
        FAIL [   0.005s] (1/3) proj tests::bad

  stderr ───

    thread panicked at 'oops'

  Cancelling due to test failure: 2 tests still running
────────────────────────────
     Summary [   0.010s] 3 tests run: 2 passed, 1 failed
        FAIL [   0.005s] (1/3) proj tests::bad
error: test run failed
"#;
        let result = filter_cargo_nextest(output);
        assert!(
            result.contains("Cancelling due to test failure"),
            "should include cancel notice: {}",
            result
        );
        assert!(
            result.contains("1 failed"),
            "should show failure count: {}",
            result
        );
        // Post-summary recap must not duplicate FAIL headers
        assert_eq!(
            result.matches("FAIL [").count(),
            1,
            "should have exactly 1 FAIL header (no post-summary duplicate): {}",
            result
        );
    }

    #[test]
    fn test_filter_cargo_nextest_summary_regex_fallback() {
        let output = r#"    Starting 5 tests across 1 binary
        PASS [   0.005s] (5/5) test_last
────────────────────────────
     Summary MALFORMED LINE
"#;
        let result = filter_cargo_nextest(output);
        assert!(
            result.contains("Summary MALFORMED"),
            "should fall back to raw summary: {}",
            result
        );
    }

    // --- Streaming handler tests ---

    use crate::core::stream::tests::run_block_filter;

    #[test]
    fn test_cargo_build_stream_success() {
        let input = "   Compiling libc v0.2.153\n   Compiling cfg-if v1.0.0\n   Compiling rtk v0.5.0\n    Finished dev [unoptimized + debuginfo] target(s) in 15.23s\n";
        let mut f = BlockStreamFilter::new(CargoBuildHandler::new());
        let result = run_block_filter(&mut f, input, 0);
        assert!(result.contains("3 crates compiled"), "got: {}", result);
        assert!(result.contains("Finished"), "got: {}", result);
        assert!(!result.contains("Compiling"), "got: {}", result);
    }

    #[test]
    fn test_cargo_build_stream_errors() {
        let input = r#"   Compiling rtk v0.5.0
error[E0308]: mismatched types
 --> src/main.rs:10:5
  |
10|     "hello"
  |     ^^^^^^^ expected `i32`, found `&str`

error: aborting due to 1 previous error
"#;
        let mut f = BlockStreamFilter::new(CargoBuildHandler::new());
        let result = run_block_filter(&mut f, input, 1);
        assert!(result.contains("E0308"), "got: {}", result);
        assert!(result.contains("mismatched types"), "got: {}", result);
        assert!(result.contains("1 errors"), "got: {}", result);
        assert!(!result.contains("aborting"), "got: {}", result);
    }

    #[test]
    fn test_cargo_test_stream_all_pass() {
        let input = r#"   Compiling rtk v0.5.0
    Finished test [unoptimized + debuginfo] target(s) in 2.53s
     Running target/debug/deps/rtk-abc123

running 15 tests
test utils::tests::test_truncate_short_string ... ok
test utils::tests::test_truncate_long_string ... ok
test utils::tests::test_strip_ansi_simple ... ok

test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
"#;
        let mut f = BlockStreamFilter::new(CargoTestHandler::new());
        let result = run_block_filter(&mut f, input, 0);
        assert!(
            result.contains("cargo test: 15 passed (1 suite, 0.01s)"),
            "got: {}",
            result
        );
        assert!(!result.contains("Compiling"), "got: {}", result);
    }

    #[test]
    fn test_cargo_test_stream_failures() {
        let input = r#"running 5 tests
test foo::test_a ... ok
test foo::test_b ... FAILED
test foo::test_c ... ok

failures:

---- foo::test_b stdout ----
thread 'foo::test_b' panicked at 'assert_eq!(1, 2)'

failures:
    foo::test_b

test result: FAILED. 4 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
"#;
        let mut f = BlockStreamFilter::new(CargoTestHandler::new());
        let result = run_block_filter(&mut f, input, 1);
        assert!(result.contains("test_b"), "got: {}", result);
        assert!(result.contains("panicked"), "got: {}", result);
    }

    #[test]
    fn test_cargo_test_stream_multi_suite() {
        let input = r#"     Running unittests src/lib.rs (target/debug/deps/rtk-abc123)

running 50 tests
test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s

     Running unittests src/main.rs (target/debug/deps/rtk-def456)

running 30 tests
test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
"#;
        let mut f = BlockStreamFilter::new(CargoTestHandler::new());
        let result = run_block_filter(&mut f, input, 0);
        assert!(
            result.contains("cargo test: 80 passed (2 suites, 0.75s)"),
            "got: {}",
            result
        );
    }

    #[test]
    fn test_cargo_test_stream_compile_error() {
        let input = r#"   Compiling rtk v0.31.0 (/workspace/projects/rtk)
error[E0425]: cannot find value `missing_symbol` in this scope
 --> tests/repro_compile_fail.rs:3:13
  |
3 |     let _ = missing_symbol;
  |             ^^^^^^^^^^^^^^ not found in this scope

For more information about this error, try `rustc --explain E0425`.
error: could not compile `rtk` (test "repro_compile_fail") due to 1 previous error
"#;
        let mut f = BlockStreamFilter::new(CargoTestHandler::new());
        let result = run_block_filter(&mut f, input, 1);
        assert!(result.contains("cargo test:"), "got: {}", result);
        assert!(result.contains("1 errors"), "got: {}", result);
    }
}