use crate::matching::{DiffKind, DiffLine, diff, match_output};
use crate::parse::ExitCode;
use crate::run::CommandResult;

// ANSI color codes
const RED: &str = "\x1b[31m";
const GREEN: &str = "\x1b[32m";
#[allow(dead_code)]
const YELLOW: &str = "\x1b[33m";
const DIM: &str = "\x1b[2m";
const RESET: &str = "\x1b[0m";

#[derive(Debug)]
pub struct FailedCommand {
    pub result: CommandResult,
    pub diff_lines: Vec<DiffLine>,
    pub exit_code_mismatch: bool,
}

#[derive(Debug)]
pub struct TestResult {
    pub path: String,
    pub passed: bool,
    pub command_count: usize,
    pub failures: Vec<FailedCommand>,
    pub error: Option<String>,
}

pub fn evaluate_file(
    path: &str,
    results: &[CommandResult],
    error: Option<&str>,
) -> TestResult {
    if let Some(err) = error {
        return TestResult {
            path: path.to_string(),
            passed: false,
            command_count: results.len(),
            failures: vec![],
            error: Some(err.to_string()),
        };
    }

    let mut failures = Vec::new();

    for result in results {
        let output_matches = match_output(&result.command.expected, &result.actual);

        let exit_code_mismatch = match &result.command.exit_code {
            ExitCode::Default => result.exit_code != 0,
            ExitCode::Any => result.exit_code == 0,
            ExitCode::Code(expected) => result.exit_code != *expected,
        };

        if !output_matches || exit_code_mismatch {
            failures.push(FailedCommand {
                result: result.clone(),
                diff_lines: if output_matches {
                    vec![]
                } else {
                    diff(&result.command.expected, &result.actual)
                },
                exit_code_mismatch,
            });
        }
    }

    TestResult {
        path: path.to_string(),
        passed: failures.is_empty(),
        command_count: results.len(),
        failures,
        error: None,
    }
}

pub fn format_failure(test: &TestResult) -> String {
    let mut lines = Vec::new();

    lines.push(format!("{RED}FAIL {}{RESET}", test.path));

    if let Some(ref err) = test.error {
        lines.push(format!("  {RED}{err}{RESET}"));
        return lines.join("\n");
    }

    for failure in &test.failures {
        lines.push(String::new());
        lines.push(format!("  {DIM}${RESET} {}", failure.result.command.command));

        if !failure.diff_lines.is_empty() {
            let mut expected_lines = Vec::new();
            let mut actual_lines = Vec::new();

            for dl in &failure.diff_lines {
                let text = if dl.kind == DiffKind::Context {
                    format!("{DIM}{}{RESET}", dl.text)
                } else {
                    dl.text.clone()
                };

                match dl.kind {
                    DiffKind::Expected | DiffKind::Equal | DiffKind::Context => {
                        let prefix = if dl.kind == DiffKind::Expected {
                            format!("{GREEN}  > {RESET}")
                        } else {
                            "    ".to_string()
                        };
                        expected_lines.push(format!("{prefix}{text}"));
                    }
                    _ => {}
                }

                match dl.kind {
                    DiffKind::Actual | DiffKind::Equal | DiffKind::Context => {
                        let prefix = if dl.kind == DiffKind::Actual {
                            format!("{RED}  > {RESET}")
                        } else {
                            "    ".to_string()
                        };
                        actual_lines.push(format!("{prefix}{text}"));
                    }
                    _ => {}
                }
            }

            lines.push(format!("{GREEN}  expected:{RESET}"));
            lines.extend(expected_lines);
            lines.push(format!("{RED}  actual:{RESET}"));
            lines.extend(actual_lines);
        }

        if failure.exit_code_mismatch {
            let expected = match &failure.result.command.exit_code {
                ExitCode::Default => "0".to_string(),
                ExitCode::Any => "non-zero".to_string(),
                ExitCode::Code(c) => c.to_string(),
            };
            lines.push(format!("{GREEN}  expected exit code: {expected}{RESET}"));
            lines.push(format!("{RED}  actual exit code: {}{RESET}", failure.result.exit_code));
        }
    }

    lines.join("\n")
}

pub fn format_summary(
    results: &[TestResult],
    elapsed_ms: f64,
    single_file: Option<&str>,
) -> String {
    let total_commands: usize = results.iter().map(|r| r.command_count).sum();
    let failed_commands: usize = results.iter().map(|r| r.failures.len()).sum();
    let passed_commands = total_commands - failed_commands;

    let mut parts = Vec::new();
    if passed_commands > 0 {
        parts.push(format!("{GREEN}{passed_commands} passed{RESET}"));
    }
    if failed_commands > 0 {
        parts.push(format!("{RED}{failed_commands} failed{RESET}"));
    }

    let time = if elapsed_ms < 1000.0 {
        format!("{}ms", elapsed_ms.round() as u64)
    } else {
        format!("{:.1}s", elapsed_ms / 1000.0)
    };

    let label = match single_file {
        Some(f) => format!(" in {f}"),
        None => String::new(),
    };

    format!("{}{label} {DIM}[{time}]{RESET}", parts.join(", "))
}

/// Print a green dot for pass, red F for fail.
pub fn print_dot(passed: bool) {
    use std::io::{Write, stdout};
    let s = if passed {
        format!("{GREEN}.{RESET}")
    } else {
        format!("{RED}F{RESET}")
    };
    let _ = stdout().write_all(s.as_bytes());
    let _ = stdout().flush();
}
