//! Compliance score renderers.
//!
//! Each renderer takes a [`ComplianceScore`] plus the per-rule check
//! results and emits a string in one of four formats: pretty (ANSI for
//! the terminal), JSON (machine-readable, schema-stable), Markdown (for
//! PR comments and READMEs), and HTML (single-file, self-contained).
//!
//! Why split by format rather than by trait: each output has different
//! constraints (ANSI codes vs CSS vs Markdown emoji) and a shared trait
//! would force a lowest-common-denominator API. Plain functions keep the
//! per-format expressiveness intact.

pub mod html;
pub mod json;
pub mod markdown;
pub mod pretty;

pub use html::render_html;
pub use json::render_json;
pub use markdown::render_markdown;
pub use pretty::render_pretty;

use super::official::CheckResult;
use super::scoring::{ComplianceScore, Grade, Section};

/// Inputs every renderer needs: the score plus the per-rule check
/// results. Bundled so each renderer takes one argument.
#[derive(Debug, Clone)]
pub struct RenderInputs<'a> {
    /// Scored run.
    pub score: &'a ComplianceScore,
    /// Per-rule check results from the runner.
    pub results: &'a [CheckResult],
    /// Server label printed in the report header. Free-form; the CLI
    /// fills it from `--server-url` or the suite name.
    pub server_label: &'a str,
}

/// Shared promotion-hint helper.
///
/// Returns the human-readable next-step suggestion that every renderer
/// shows below the grade. Empty string when the run is already at A+.
pub fn promotion_hint(score: &ComplianceScore) -> String {
    match score.grade {
        Grade::APlus => String::new(),
        Grade::F => format!(
            "Fix the {} failed MUST check(s) to leave grade F.",
            score.failed_must_checks.len()
        ),
        Grade::A => {
            if score.missing_for_a_plus.is_empty() {
                "Currently A. Add MAY rules to applicable sections to promote to A+.".into()
            } else {
                let total = score.missing_for_a_plus.len();
                let examples: Vec<String> = score
                    .missing_for_a_plus
                    .iter()
                    .take(3)
                    .map(|id| id.to_string())
                    .collect();
                // WOR-1507: name the remainder so a server needing 8 MAY rules
                // is not told the 3 shown are the whole remediation list.
                let more = if total > examples.len() {
                    format!(" (and {} more)", total - examples.len())
                } else {
                    String::new()
                };
                format!(
                    "Currently A. To reach A+, implement these MAY rules: {}{more}.",
                    examples.join(", ")
                )
            }
        }
        Grade::B => "Currently B. Pass the remaining SHOULD rules to reach A.".into(),
        Grade::C => "Currently C. Pass at least 80% of SHOULD rules to reach B.".into(),
        Grade::D => "Currently D. Pass at least 50% of SHOULD rules to reach C.".into(),
    }
}

/// Sections in display order. Renderers iterate via this helper so the
/// table layout stays stable across formats.
pub fn ordered_sections() -> &'static [Section] {
    Section::ALL
}
