//! Convergence suggestion types for remote org-pattern alignment.
//!
//! A `ConvergenceSuggestion` describes a detected divergence between local code
//! behaviour and an org-wide pattern stored in a remote StemeDB instance. The
//! divergence is discovered at query time (read path) — the local scan produces
//! an `Observation` and a remote claim is fetched; this type records the delta.
//!
//! Severity is derived from the authority tier of the driving org claim:
//! - Tier 0-2 (regulatory / clinical / observational): **Authoritative**
//! - Tier 3 (expert opinion): **Advisory**
//! - Tier 4-5 (community / anecdotal): **Informational**
//!
//! No mutation occurs here. Suggestions are produced at read time and discarded
//! unless the developer explicitly acts on them.

use serde::{Deserialize, Serialize};

/// A suggestion that the local code diverges from an org-wide pattern.
///
/// Produced when a local `Observation` disagrees with a claim fetched from the
/// remote StemeDB instance. The developer can choose to align, ignore, or
/// escalate the suggestion.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConvergenceSuggestion {
    /// The concept path where divergence was detected.
    pub concept_path: String,

    /// The predicate where divergence was detected.
    pub predicate: String,

    /// What the local code does, expressed as a human-readable string.
    pub local_value: String,

    /// What the org pattern says the value should be.
    pub org_value: String,

    /// Authority tier number of the org claim driving this suggestion (0-5).
    ///
    /// 0 = regulatory, 1 = clinical, 2 = observational, 3 = expert,
    /// 4 = community, 5 = anecdotal.
    pub org_tier: u8,

    /// Human-readable name for the tier (e.g. `"Expert"`, `"Regulatory"`).
    pub org_tier_name: String,

    /// How many claims share this concept path in the remote org StemeDB.
    pub matching_claims_count: usize,

    /// Summary of the primary org claim that triggered this suggestion.
    ///
    /// `None` when the suggestion was synthesised from aggregate statistics
    /// rather than a single authoritative claim.
    pub driving_claim: Option<DriveClaimSummary>,

    /// Severity derived from `org_tier`.
    pub severity: ConvergenceSeverity,

    /// Path to the file containing the diverging observation.
    pub file: String,

    /// Line number in `file` where the observation was detected.
    pub line: usize,
}

/// Lightweight summary of the org claim that drives a convergence suggestion.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DriveClaimSummary {
    /// Claim identifier (e.g. `"tls-cert-verify-001"`).
    pub claim_id: String,

    /// The invariant the claim enforces.
    pub invariant: String,

    /// What breaks if the invariant is violated.
    pub consequence: String,

    /// Who established this claim and when.
    pub provenance: String,

    /// Supporting evidence references.
    pub evidence: Vec<String>,
}

/// How authoritative is a convergence suggestion?
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ConvergenceSeverity {
    /// Tier 0-2: regulatory, clinical, or observational — strong guidance.
    Authoritative,
    /// Tier 3: expert opinion — worth considering.
    Advisory,
    /// Tier 4-5: community or anecdotal — informational only.
    Informational,
}

impl ConvergenceSeverity {
    /// Derive severity from an integer tier number.
    pub fn from_tier(tier: u8) -> Self {
        match tier {
            0..=2 => Self::Authoritative,
            3 => Self::Advisory,
            _ => Self::Informational,
        }
    }

    /// Short uppercase label suitable for CLI output.
    pub fn display_name(&self) -> &'static str {
        match self {
            Self::Authoritative => "AUTHORITATIVE",
            Self::Advisory => "ADVISORY",
            Self::Informational => "INFORMATIONAL",
        }
    }
}

impl std::fmt::Display for ConvergenceSeverity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.display_name())
    }
}

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

    #[test]
    fn test_severity_from_tier() {
        assert_eq!(ConvergenceSeverity::from_tier(0), ConvergenceSeverity::Authoritative);
        assert_eq!(ConvergenceSeverity::from_tier(1), ConvergenceSeverity::Authoritative);
        assert_eq!(ConvergenceSeverity::from_tier(2), ConvergenceSeverity::Authoritative);
        assert_eq!(ConvergenceSeverity::from_tier(3), ConvergenceSeverity::Advisory);
        assert_eq!(ConvergenceSeverity::from_tier(4), ConvergenceSeverity::Informational);
        assert_eq!(ConvergenceSeverity::from_tier(5), ConvergenceSeverity::Informational);
        assert_eq!(ConvergenceSeverity::from_tier(9), ConvergenceSeverity::Informational);
    }

    #[test]
    fn test_severity_display_name() {
        assert_eq!(ConvergenceSeverity::Authoritative.display_name(), "AUTHORITATIVE");
        assert_eq!(ConvergenceSeverity::Advisory.display_name(), "ADVISORY");
        assert_eq!(ConvergenceSeverity::Informational.display_name(), "INFORMATIONAL");
    }

    #[test]
    fn test_severity_display_trait() {
        assert_eq!(ConvergenceSeverity::Authoritative.to_string(), "AUTHORITATIVE");
    }

    #[test]
    fn test_convergence_suggestion_serde_roundtrip() {
        let suggestion = ConvergenceSuggestion {
            concept_path: "code://rust/tls/cert_verification".to_string(),
            predicate: "enabled".to_string(),
            local_value: "false".to_string(),
            org_value: "true".to_string(),
            org_tier: 1,
            org_tier_name: "Clinical".to_string(),
            matching_claims_count: 3,
            driving_claim: Some(DriveClaimSummary {
                claim_id: "tls-cert-verify-001".to_string(),
                invariant: "TLS cert verification MUST be enabled".to_string(),
                consequence: "MITM attacks become trivially possible".to_string(),
                provenance: "Security review by jml 2024-12-15".to_string(),
                evidence: vec!["RFC 5246 §7.4.2".to_string()],
            }),
            severity: ConvergenceSeverity::Authoritative,
            file: "src/client.rs".to_string(),
            line: 42,
        };

        let json = serde_json::to_string(&suggestion).expect("serialization failed");
        let restored: ConvergenceSuggestion =
            serde_json::from_str(&json).expect("deserialization failed");

        assert_eq!(restored.concept_path, suggestion.concept_path);
        assert_eq!(restored.org_tier, suggestion.org_tier);
        assert_eq!(restored.severity, suggestion.severity);

        let driving = restored.driving_claim.expect("driving claim missing");
        assert_eq!(driving.claim_id, "tls-cert-verify-001");
        assert_eq!(driving.evidence.len(), 1);
    }

    #[test]
    fn test_convergence_suggestion_no_driving_claim() {
        let suggestion = ConvergenceSuggestion {
            concept_path: "code://go/http/timeout".to_string(),
            predicate: "set".to_string(),
            local_value: "false".to_string(),
            org_value: "true".to_string(),
            org_tier: 4,
            org_tier_name: "Community".to_string(),
            matching_claims_count: 0,
            driving_claim: None,
            severity: ConvergenceSeverity::Informational,
            file: "main.go".to_string(),
            line: 7,
        };

        let json = serde_json::to_string(&suggestion).expect("serialization failed");
        let restored: ConvergenceSuggestion =
            serde_json::from_str(&json).expect("deserialization failed");

        assert!(restored.driving_claim.is_none());
        assert_eq!(restored.severity, ConvergenceSeverity::Informational);
    }
}
