//! ANSI-coloured terminal renderer for a [`ComplianceScore`].
//!
//! Colors follow ADR-0012 badges: A+ emerald, A green, B lime, C yellow,
//! D orange, F red, ungraded gray.

use std::fmt::Write;

use super::super::scoring::{ComplianceScore, Grade, Section, SectionScore};
use super::{ordered_sections, promotion_hint, RenderInputs};

const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";

fn grade_color(grade: Grade) -> &'static str {
    match grade {
        // Truecolor escape codes; fall back gracefully on terminals that
        // do not understand them (they ignore the unknown SGR params).
        Grade::APlus => "\x1b[38;2;16;185;129m", // emerald-500
        Grade::A => "\x1b[38;2;34;197;94m",      // green-500
        Grade::B => "\x1b[38;2;132;204;22m",     // lime-500
        Grade::C => "\x1b[38;2;234;179;8m",      // yellow-500
        Grade::D => "\x1b[38;2;249;115;22m",     // orange-500
        Grade::F => "\x1b[38;2;239;68;68m",      // red-500
    }
}

const GRAY: &str = "\x1b[38;2;156;163;175m"; // gray-400 for N/A rows

/// Render the score as ANSI-coloured text suitable for a terminal.
///
/// Use `inputs.score` for the header and `inputs.results` only for
/// renderers that surface per-rule lines (this one shows failing MUSTs).
pub fn render_pretty(inputs: &RenderInputs<'_>) -> String {
    let mut out = String::new();
    let score = inputs.score;

    write_header(&mut out, inputs);
    write_section_table(&mut out, score);
    write_grade_line(&mut out, score);
    write_failed_must(&mut out, score);
    write_hint(&mut out, score);
    out
}

fn write_header(out: &mut String, inputs: &RenderInputs<'_>) {
    let _ = writeln!(
        out,
        "{BOLD}mcptest compliance{RESET} against {}",
        inputs.server_label
    );
    let _ = writeln!(out, "  Rubric version: {}", inputs.score.rubric_version);
}

fn write_section_table(out: &mut String, score: &ComplianceScore) {
    out.push('\n');
    for section in ordered_sections() {
        let Some(row) = score.sections.get(section) else {
            continue;
        };
        write_section_row(out, *section, row);
    }
    out.push('\n');
}

fn write_section_row(out: &mut String, section: Section, row: &SectionScore) {
    let label = section_label(section);
    if !row.applicable {
        let _ = writeln!(
            out,
            "  {label:<14} {GRAY}[-]{RESET}  N/A (capability not declared)"
        );
        return;
    }
    let color = grade_color(row.grade);
    let badge = format!("[{}]", row.grade.as_str());
    let _ = writeln!(
        out,
        "  {label:<14} {color}{badge:<5}{RESET}  MUST {}/{}, SHOULD {}/{}, MAY {}/{}",
        row.must_passed,
        row.must_total,
        row.should_passed,
        row.should_total,
        row.may_passed,
        row.may_total,
    );
}

fn section_label(section: Section) -> &'static str {
    match section {
        Section::Lifecycle => "Lifecycle",
        Section::Tools => "Tools",
        Section::Resources => "Resources",
        Section::Prompts => "Prompts",
        Section::TransportAuth => "Transport",
    }
}

fn write_grade_line(out: &mut String, score: &ComplianceScore) {
    let color = grade_color(score.grade);
    let _ = writeln!(
        out,
        "{BOLD}Grade:{RESET} {color}{BOLD}{}{RESET}",
        score.grade.as_str()
    );
    let _ = writeln!(out, "Score: {:.1}/100", score.numeric_score);
    let _ = writeln!(
        out,
        "Completeness: {:.0}%",
        score.completeness_score * 100.0,
    );
}

fn write_failed_must(out: &mut String, score: &ComplianceScore) {
    if score.failed_must_checks.is_empty() {
        return;
    }
    let color = grade_color(Grade::F);
    let _ = writeln!(out, "\n{color}{BOLD}Failed MUST checks:{RESET}");
    for rule_id in &score.failed_must_checks {
        let _ = writeln!(out, "  {color}{rule_id}{RESET}");
    }
}

fn write_hint(out: &mut String, score: &ComplianceScore) {
    let hint = promotion_hint(score);
    if hint.is_empty() {
        return;
    }
    let _ = writeln!(out, "\n{hint}");
}

#[cfg(test)]
mod tests {
    use super::super::super::official::{CheckResult, CheckSource};
    use super::super::super::registry::{RuleMetadata, RuleRegistry, RuleSeverity};
    use super::super::super::scoring::{score_run, Rubric};
    use super::*;
    use std::collections::BTreeSet;

    fn rule(id: &str, section: &str, severity: RuleSeverity) -> RuleMetadata {
        RuleMetadata {
            id: id.into(),
            name: format!("rule {id}"),
            category: id.split('-').next().unwrap_or("PROTO").into(),
            severity,
            applies_to: vec!["stdio".into()],
            spec_versions: vec!["v2024-11-05".into()],
            description: "x".into(),
            references: vec![],
            section: section.into(),
            applicability: "true".into(),
            spec_citation: "spec".into(),
        }
    }

    fn check(id: &str, passed: bool) -> CheckResult {
        CheckResult {
            rule_id: id.into(),
            passed,
            source: CheckSource::Local,
            message: None,
            duration_ms: 1,
        }
    }

    fn fixture_score(rules: Vec<RuleMetadata>, results: Vec<CheckResult>) -> ComplianceScore {
        let reg = RuleRegistry { version: 1, rules };
        let caps = BTreeSet::from(["tools".into()]);
        let rubric = Rubric::new(&reg, caps);
        score_run(&results, &rubric)
    }

    #[test]
    fn pretty_renders_grade_a_with_promotion_hint() {
        let score = fixture_score(
            vec![
                rule("PROTO-001", "lifecycle", RuleSeverity::Must),
                rule("PROTO-002", "lifecycle", RuleSeverity::May),
            ],
            vec![check("PROTO-001", true), check("PROTO-002", false)],
        );
        let results = vec![check("PROTO-001", true), check("PROTO-002", false)];
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &results,
            server_label: "stdio://test",
        });
        assert!(out.contains("Grade:"));
        assert!(out.contains("A"));
        assert!(out.contains("To reach A+"));
    }

    #[test]
    fn pretty_renders_grade_f_with_failed_must_list() {
        let score = fixture_score(
            vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)],
            vec![check("PROTO-001", false)],
        );
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &[check("PROTO-001", false)],
            server_label: "stdio://test",
        });
        assert!(out.contains("Failed MUST checks"));
        assert!(out.contains("PROTO-001"));
        assert!(out.contains("F"));
    }

    #[test]
    fn pretty_renders_grade_b_with_should_hint() {
        let mut rules = vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)];
        for i in 0..5 {
            rules.push(rule(
                &format!("PROTO-{i:03}-S"),
                "lifecycle",
                RuleSeverity::Should,
            ));
        }
        let mut results = vec![check("PROTO-001", true)];
        for i in 0..5 {
            results.push(check(&format!("PROTO-{i:03}-S"), i < 4));
        }
        let score = fixture_score(rules, results.clone());
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &results,
            server_label: "stdio://test",
        });
        assert!(out.contains("Currently B"));
    }

    #[test]
    fn pretty_marks_non_applicable_section_as_na() {
        let score = fixture_score(
            vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)],
            vec![check("PROTO-001", true)],
        );
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &[check("PROTO-001", true)],
            server_label: "stdio://test",
        });
        // Resources is not declared in the fixture, so its row must
        // render as the gray N/A entry rather than a graded row.
        assert!(out.contains("Resources"));
        assert!(out.contains("N/A"));
    }

    #[test]
    fn pretty_emits_a_plus_when_every_section_passes_a_may() {
        // Single applicable section (lifecycle) with one MAY pass -> A+.
        let score = fixture_score(
            vec![
                rule("PROTO-001", "lifecycle", RuleSeverity::Must),
                rule("PROTO-010", "lifecycle", RuleSeverity::May),
            ],
            vec![check("PROTO-001", true), check("PROTO-010", true)],
        );
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &[],
            server_label: "stdio://test",
        });
        assert!(out.contains("A+"));
        assert!(!out.contains("To reach A+"));
    }

    #[test]
    fn pretty_renders_grade_c_with_should_hint() {
        // 5 of 10 SHOULDs pass = 50% exactly = C.
        let mut rules = vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)];
        for i in 0..10 {
            rules.push(rule(
                &format!("PROTO-{i:03}-S"),
                "lifecycle",
                RuleSeverity::Should,
            ));
        }
        let mut results = vec![check("PROTO-001", true)];
        for i in 0..10 {
            results.push(check(&format!("PROTO-{i:03}-S"), i < 5));
        }
        let score = fixture_score(rules, results.clone());
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &results,
            server_label: "stdio://test",
        });
        assert!(out.contains("Currently C"));
    }

    #[test]
    fn pretty_renders_grade_d_with_should_hint() {
        // 4 of 10 SHOULDs pass = 40%, below 50% = D.
        let mut rules = vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)];
        for i in 0..10 {
            rules.push(rule(
                &format!("PROTO-{i:03}-S"),
                "lifecycle",
                RuleSeverity::Should,
            ));
        }
        let mut results = vec![check("PROTO-001", true)];
        for i in 0..10 {
            results.push(check(&format!("PROTO-{i:03}-S"), i < 4));
        }
        let score = fixture_score(rules, results.clone());
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &results,
            server_label: "stdio://test",
        });
        assert!(out.contains("Currently D"));
    }

    #[test]
    fn pretty_handles_single_section_run() {
        // Only one section (lifecycle) with one rule. Output should still
        // render the header, the section row, and the grade line.
        let score = fixture_score(
            vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)],
            vec![check("PROTO-001", true)],
        );
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &[],
            server_label: "stdio://x",
        });
        assert!(out.contains("Lifecycle"));
        assert!(out.contains("Grade:"));
    }

    #[test]
    fn pretty_handles_run_with_no_may_rules() {
        // No MAY rules at all: applicable sections with all MUSTs and
        // SHOULDs passing promote vacuously to A+.
        let score = fixture_score(
            vec![
                rule("PROTO-001", "lifecycle", RuleSeverity::Must),
                rule("PROTO-002", "lifecycle", RuleSeverity::Should),
            ],
            vec![check("PROTO-001", true), check("PROTO-002", true)],
        );
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &[],
            server_label: "stdio://x",
        });
        assert!(out.contains("A+"));
    }

    #[test]
    fn pretty_renders_when_no_capabilities_declared() {
        // No declared capabilities. Resources, Tools, and Prompts should
        // render as N/A; Lifecycle and Transport stay applicable.
        let reg = RuleRegistry {
            version: 1,
            rules: vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)],
        };
        let rubric = Rubric::new(&reg, BTreeSet::new());
        let score = score_run(&[check("PROTO-001", true)], &rubric);
        let out = render_pretty(&RenderInputs {
            score: &score,
            results: &[],
            server_label: "stdio://x",
        });
        assert!(out.contains("Tools"));
        assert!(out.contains("N/A"));
        // Lifecycle is always applicable so it should not be N/A.
        assert!(out.contains("Lifecycle"));
    }
}
