//! Protocol-revision feature gating for the catalog diff.
//!
//! Why this exists: a tool catalog can declare fields (structured-output
//! schemas, annotations, titles, server instructions) that only became
//! legal at a specific MCP revision. When two snapshots were captured
//! against different revisions, a field "appearing" can mean the server
//! adopted a feature the older snapshot simply could not express, not a
//! real contract change. This module computes, from a pair of protocol
//! versions, which features both snapshots could express, so the diff can
//! suppress (and footnote) comparisons that would be noise.

use crate::protocol::ProtocolVersion;

/// A protocol-gated catalog feature.
///
/// Each variant maps to one field (or family of fields) that became legal
/// at a known revision; the diff consults [`shared_feature_flags`] to
/// decide whether comparing that field across two snapshots is meaningful.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FeatureFlag {
    /// Per-tool `annotations` object (readOnlyHint, destructiveHint, ...).
    Annotations,
    /// First-class `title` field on tools.
    Titles,
    /// Per-tool `outputSchema` declaring structured tool output.
    StructuredOutput,
    /// Server-level `instructions` text.
    ServerInstructions,
    /// Tasks / long-running operations surface.
    Tasks,
    /// Per-tool / per-server `icons`.
    Icons,
}

/// The protocol revision that first made `flag` legal on the wire.
///
/// Pinned to the published MCP changelog; verified against the
/// [`ProtocolVersion`] variant docs. Annotations shipped in the first
/// public revision; structured output, titles, and server instructions in
/// the 2025-06-18 production revision; icons in 2025-11-25; tasks in the
/// 2026-07-28 stateless revision.
pub fn introduced_in(flag: FeatureFlag) -> ProtocolVersion {
    match flag {
        FeatureFlag::Annotations => ProtocolVersion::V2024_11_05,
        FeatureFlag::Titles => ProtocolVersion::V2025_06_18,
        FeatureFlag::StructuredOutput => ProtocolVersion::V2025_06_18,
        FeatureFlag::ServerInstructions => ProtocolVersion::V2025_06_18,
        FeatureFlag::Icons => ProtocolVersion::V2025_11_25,
        FeatureFlag::Tasks => ProtocolVersion::V2026_07_28,
    }
}

/// Total order over protocol revisions as a small integer.
///
/// Why a hand-rolled map rather than `#[derive(Ord)]`: [`ProtocolVersion`]
/// deliberately derives only `Eq`/`Hash` (its variant order is not a
/// public guarantee), so we encode the chronological rank here where the
/// gating logic can rely on it without leaking an ordering onto the type.
pub fn ordinal(v: ProtocolVersion) -> u8 {
    match v {
        ProtocolVersion::V2024_11_05 => 0,
        ProtocolVersion::V2025_03_26 => 1,
        ProtocolVersion::V2025_06_18 => 2,
        ProtocolVersion::V2025_11_25 => 3,
        ProtocolVersion::V2026_03_26 => 4,
        ProtocolVersion::V2026_07_28 => 5,
    }
}

/// True when `version` is at or past the revision that introduced `flag`.
///
/// Uses [`ordinal`] for the comparison so the answer does not depend on
/// the enum's declaration order.
pub fn supports(version: ProtocolVersion, flag: FeatureFlag) -> bool {
    ordinal(version) >= ordinal(introduced_in(flag))
}

/// The features both snapshots can express: the AND-intersection of each
/// version's supported set.
///
/// The diff only compares a gated field when its flag is in this list, so
/// a field that exists on the newer snapshot but was inexpressible at the
/// older snapshot's revision is treated as "not comparable" rather than a
/// spurious change.
pub fn shared_feature_flags(a: ProtocolVersion, b: ProtocolVersion) -> Vec<FeatureFlag> {
    const ALL: [FeatureFlag; 6] = [
        FeatureFlag::Annotations,
        FeatureFlag::Titles,
        FeatureFlag::StructuredOutput,
        FeatureFlag::ServerInstructions,
        FeatureFlag::Tasks,
        FeatureFlag::Icons,
    ];
    ALL.into_iter()
        .filter(|&flag| supports(a, flag) && supports(b, flag))
        .collect()
}

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

    #[test]
    fn shared_flags_drop_features_the_older_version_cannot_express() {
        // 2024-11-05 predates structured output, titles, server
        // instructions, icons, and tasks; only annotations is shared with
        // a modern revision.
        let shared =
            shared_feature_flags(ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18);
        assert!(shared.contains(&FeatureFlag::Annotations));
        assert!(!shared.contains(&FeatureFlag::StructuredOutput));
        assert!(!shared.contains(&FeatureFlag::Titles));
        assert!(!shared.contains(&FeatureFlag::ServerInstructions));
        assert!(!shared.contains(&FeatureFlag::Icons));
        assert!(!shared.contains(&FeatureFlag::Tasks));
    }

    #[test]
    fn same_modern_version_shares_structured_output() {
        let shared =
            shared_feature_flags(ProtocolVersion::V2025_06_18, ProtocolVersion::V2025_06_18);
        assert!(shared.contains(&FeatureFlag::StructuredOutput));
        assert!(shared.contains(&FeatureFlag::Titles));
        assert!(shared.contains(&FeatureFlag::ServerInstructions));
        assert!(shared.contains(&FeatureFlag::Annotations));
        // Icons and tasks postdate 2025-06-18, so neither is shared.
        assert!(!shared.contains(&FeatureFlag::Icons));
        assert!(!shared.contains(&FeatureFlag::Tasks));
    }

    #[test]
    fn ordinal_is_chronological_not_declaration_order() {
        assert!(ordinal(ProtocolVersion::V2024_11_05) < ordinal(ProtocolVersion::V2025_06_18));
        assert!(ordinal(ProtocolVersion::V2025_06_18) < ordinal(ProtocolVersion::V2026_07_28));
    }

    #[test]
    fn supports_is_true_at_the_introducing_revision() {
        assert!(supports(
            ProtocolVersion::V2025_06_18,
            FeatureFlag::StructuredOutput
        ));
        assert!(!supports(
            ProtocolVersion::V2025_03_26,
            FeatureFlag::StructuredOutput
        ));
    }
}
