//! Markdown renderer for a [`ComplianceScore`].
//!
//! Tuned for PR comments and README badges: top-line grade, per-section
//! table, embedded badge URL. Emoji map onto the ADR-0012 color scheme.

use std::fmt::Write;

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

fn grade_emoji(grade: Grade) -> &'static str {
    match grade {
        Grade::APlus => "\u{1F7E2}", // green circle
        Grade::A => "\u{1F7E2}",     // green circle
        Grade::B => "\u{1F7E1}",     // yellow circle (closest to lime)
        Grade::C => "\u{1F7E1}",     // yellow circle
        Grade::D => "\u{1F7E0}",     // orange circle
        Grade::F => "\u{1F534}",     // red circle
    }
}

/// Render the score as GitHub-flavoured Markdown.
pub fn render_markdown(inputs: &RenderInputs<'_>) -> String {
    let score = inputs.score;
    let mut out = String::new();
    write_header(&mut out, score, inputs.server_label);
    write_badge(&mut out, score);
    write_section_table(&mut out, score);
    write_failed_must(&mut out, score);
    write_hint(&mut out, score);
    out
}

fn write_header(out: &mut String, score: &ComplianceScore, server: &str) {
    let _ = writeln!(
        out,
        "# Compliance grade: {} {}",
        grade_emoji(score.grade),
        score.grade.as_str()
    );
    out.push('\n');
    let _ = writeln!(out, "- Server: `{server}`");
    let _ = writeln!(out, "- Score: **{:.1}/100**", score.numeric_score);
    let _ = writeln!(
        out,
        "- Completeness: {:.0}%",
        score.completeness_score * 100.0,
    );
    let _ = writeln!(out, "- Rubric version: `{}`", score.rubric_version);
    out.push('\n');
}

fn write_badge(out: &mut String, score: &ComplianceScore) {
    let label = score.grade.as_str().replace('+', "%2B");
    let color = match score.grade {
        Grade::APlus | Grade::A => "brightgreen",
        Grade::B => "yellowgreen",
        Grade::C => "yellow",
        Grade::D => "orange",
        Grade::F => "red",
    };
    let _ = writeln!(
        out,
        "![mcptest compliance](https://img.shields.io/badge/mcptest-{label}-{color})"
    );
    out.push('\n');
}

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

fn write_section_table(out: &mut String, score: &ComplianceScore) {
    let _ = writeln!(out, "## Section breakdown\n");
    let _ = writeln!(out, "| Section | Grade | MUST | SHOULD | MAY |");
    let _ = writeln!(out, "| --- | --- | --- | --- | --- |");
    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} | N/A | - | - | - |");
        return;
    }
    let _ = writeln!(
        out,
        "| {} | {} {} | {}/{} | {}/{} | {}/{} |",
        label,
        grade_emoji(row.grade),
        row.grade.as_str(),
        row.must_passed,
        row.must_total,
        row.should_passed,
        row.should_total,
        row.may_passed,
        row.may_total,
    );
}

fn write_failed_must(out: &mut String, score: &ComplianceScore) {
    if score.failed_must_checks.is_empty() {
        return;
    }
    let _ = writeln!(out, "## Failed MUST checks\n");
    for rule_id in &score.failed_must_checks {
        let _ = writeln!(out, "- `{rule_id}`");
    }
    out.push('\n');
}

fn write_hint(out: &mut String, score: &ComplianceScore) {
    let hint = promotion_hint(score);
    if hint.is_empty() {
        return;
    }
    let _ = writeln!(out, "> {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 markdown_renders_grade_in_heading() {
        let score = fixture_score(
            vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)],
            vec![check("PROTO-001", true)],
        );
        let out = render_markdown(&RenderInputs {
            score: &score,
            results: &[check("PROTO-001", true)],
            server_label: "stdio://x",
        });
        assert!(out.starts_with("# Compliance grade:"));
        assert!(out.contains("Section breakdown"));
    }

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

    #[test]
    fn markdown_omits_failed_must_section_when_clean() {
        let score = fixture_score(
            vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)],
            vec![check("PROTO-001", true)],
        );
        let out = render_markdown(&RenderInputs {
            score: &score,
            results: &[],
            server_label: "x",
        });
        assert!(!out.contains("## Failed MUST checks"));
    }

    #[test]
    fn markdown_marks_na_sections() {
        let score = fixture_score(
            vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)],
            vec![check("PROTO-001", true)],
        );
        let out = render_markdown(&RenderInputs {
            score: &score,
            results: &[],
            server_label: "x",
        });
        // Resources is not declared in fixture_score's capability set.
        assert!(out.contains("| Resources | N/A |"));
    }

    #[test]
    fn markdown_emits_shields_badge_url() {
        let score = fixture_score(
            vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)],
            vec![check("PROTO-001", true)],
        );
        let out = render_markdown(&RenderInputs {
            score: &score,
            results: &[],
            server_label: "x",
        });
        assert!(out.contains("img.shields.io/badge/mcptest"));
    }

    #[test]
    fn markdown_encodes_a_plus_in_badge() {
        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_markdown(&RenderInputs {
            score: &score,
            results: &[],
            server_label: "x",
        });
        // `+` becomes `%2B` in the shields URL.
        assert!(out.contains("mcptest-A%2B-brightgreen"));
    }

    #[test]
    fn markdown_renders_grade_b_badge() {
        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_markdown(&RenderInputs {
            score: &score,
            results: &results,
            server_label: "x",
        });
        assert!(out.contains("mcptest-B-yellowgreen"));
    }

    #[test]
    fn markdown_renders_grade_c_badge() {
        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_markdown(&RenderInputs {
            score: &score,
            results: &results,
            server_label: "x",
        });
        assert!(out.contains("mcptest-C-yellow"));
    }

    #[test]
    fn markdown_renders_grade_d_badge() {
        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_markdown(&RenderInputs {
            score: &score,
            results: &results,
            server_label: "x",
        });
        assert!(out.contains("mcptest-D-orange"));
    }

    #[test]
    fn markdown_renders_grade_f_badge() {
        let score = fixture_score(
            vec![rule("PROTO-001", "lifecycle", RuleSeverity::Must)],
            vec![check("PROTO-001", false)],
        );
        let out = render_markdown(&RenderInputs {
            score: &score,
            results: &[],
            server_label: "x",
        });
        assert!(out.contains("mcptest-F-red"));
    }
}
