//! SDK-tier scoring for a conformance run (SEP-2484).
//!
//! The 2026-07-28 spec ties the publicly visible SDK tier to the
//! conformance suite's MUST / SHOULD pass counts. mcptest aggregates a
//! run's results into a single [`Tier`] so a CI badge can summarize
//! the run with one letter and a percentage:
//!
//! - **Tier 1** ("gold"): every MUST check passes and at least 95 % of
//!   SHOULD checks pass. Reserved for SDKs that ship the full surface.
//! - **Tier 2** ("silver"): every MUST check passes and at least 70 %
//!   of SHOULD checks pass. The mainstream tier.
//! - **Tier 3** ("bronze"): every MUST check passes. SHOULD coverage
//!   below 70 % but no MUST regressions.
//! - **Fail**: at least one MUST check failed. No tier.
//!
//! The thresholds match the spec table exactly. MAY checks affect
//! neither the tier nor the percentage; they are reported for the
//! operator and never gate the badge.

use serde::{Deserialize, Serialize};

/// Inputs to [`score_tier`].
///
/// Pass / total counts come from whoever ran the conformance corpus
/// (today, the operator hand-counts results; the eventual
/// `mcptest conformance` CLI will populate this struct from the
/// scenario verdicts).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TierInput {
    /// Number of MUST checks that passed.
    pub must_passed: u32,
    /// Total MUST checks in the corpus.
    pub must_total: u32,
    /// Number of SHOULD checks that passed.
    pub should_passed: u32,
    /// Total SHOULD checks in the corpus.
    pub should_total: u32,
}

/// SDK tier verdict assigned to a conformance run.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "tier", rename_all = "kebab-case")]
pub enum Tier {
    /// Every MUST passes; SHOULD pass rate is at least 95 %.
    Tier1,
    /// Every MUST passes; SHOULD pass rate is at least 70 %.
    Tier2,
    /// Every MUST passes; SHOULD pass rate is below 70 %.
    Tier3,
    /// At least one MUST check failed; no tier awarded.
    Fail,
}

impl Tier {
    /// Short letter form for badges and short reports
    /// (`T1` / `T2` / `T3` / `F`).
    pub fn badge(self) -> &'static str {
        match self {
            Self::Tier1 => "T1",
            Self::Tier2 => "T2",
            Self::Tier3 => "T3",
            Self::Fail => "F",
        }
    }
}

/// SHOULD pass-rate threshold for Tier 1 (per SEP-2484).
pub const TIER1_SHOULD_THRESHOLD: f64 = 0.95;

/// SHOULD pass-rate threshold for Tier 2 (per SEP-2484).
pub const TIER2_SHOULD_THRESHOLD: f64 = 0.70;

/// Decide the tier from the aggregated pass / total counts.
///
/// A corpus with zero MUST checks is treated as Tier 3 by default:
/// there is nothing to fail, so the floor is the lowest passing
/// tier. A corpus with zero SHOULD checks treats the pass rate as
/// `1.0` (vacuously satisfied), so the tier rises with no SHOULD
/// gate to fail.
pub fn score_tier(input: TierInput) -> Tier {
    if input.must_passed < input.must_total {
        return Tier::Fail;
    }
    let should_rate = if input.should_total == 0 {
        1.0
    } else {
        f64::from(input.should_passed) / f64::from(input.should_total)
    };
    if should_rate >= TIER1_SHOULD_THRESHOLD {
        Tier::Tier1
    } else if should_rate >= TIER2_SHOULD_THRESHOLD {
        Tier::Tier2
    } else {
        Tier::Tier3
    }
}

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

    fn input(must: (u32, u32), should: (u32, u32)) -> TierInput {
        TierInput {
            must_passed: must.0,
            must_total: must.1,
            should_passed: should.0,
            should_total: should.1,
        }
    }

    #[test]
    fn perfect_run_lands_in_tier_one() {
        let r = score_tier(input((10, 10), (20, 20)));
        assert_eq!(r, Tier::Tier1);
        assert_eq!(r.badge(), "T1");
    }

    #[test]
    fn must_failure_floors_the_verdict_at_fail_regardless_of_should_rate() {
        let r = score_tier(input((9, 10), (20, 20)));
        assert_eq!(r, Tier::Fail);
        assert_eq!(r.badge(), "F");
    }

    #[test]
    fn tier_one_at_exactly_ninety_five_percent_should() {
        // 19 / 20 = 0.95 exactly.
        let r = score_tier(input((10, 10), (19, 20)));
        assert_eq!(r, Tier::Tier1);
    }

    #[test]
    fn tier_two_at_seventy_percent_should() {
        // 14 / 20 = 0.70 exactly.
        let r = score_tier(input((10, 10), (14, 20)));
        assert_eq!(r, Tier::Tier2);
    }

    #[test]
    fn tier_two_just_below_ninety_five_percent_should() {
        // 18 / 20 = 0.90 -> Tier 2.
        let r = score_tier(input((10, 10), (18, 20)));
        assert_eq!(r, Tier::Tier2);
    }

    #[test]
    fn tier_three_just_below_seventy_percent_should() {
        // 13 / 20 = 0.65 -> Tier 3.
        let r = score_tier(input((10, 10), (13, 20)));
        assert_eq!(r, Tier::Tier3);
        assert_eq!(r.badge(), "T3");
    }

    #[test]
    fn corpus_with_no_should_checks_yields_tier_one_when_must_is_clean() {
        let r = score_tier(input((10, 10), (0, 0)));
        // SHOULD rate is treated as 1.0 (vacuously satisfied), so the
        // run lifts to Tier 1.
        assert_eq!(r, Tier::Tier1);
    }

    #[test]
    fn corpus_with_no_must_checks_still_passes_through_tier_one() {
        // No MUST gate at all, perfect SHOULD: lifts to Tier 1.
        let r = score_tier(input((0, 0), (20, 20)));
        assert_eq!(r, Tier::Tier1);
    }

    #[test]
    fn tier_serializes_to_a_stable_kebab_case_tag() {
        let json = serde_json::to_value(Tier::Tier1).expect("serializes");
        assert_eq!(json, serde_json::json!({"tier": "tier1"}));
        let json = serde_json::to_value(Tier::Fail).expect("serializes");
        assert_eq!(json, serde_json::json!({"tier": "fail"}));
    }
}
