//! Promotion request and result types for claim tier advancement.
//!
//! Promotion raises a claim to a higher authority tier (lower tier number),
//! recording stronger evidence for the claim's invariant. The original claim
//! is never mutated; the promotion creates a new claim that supersedes it
//! (append-only invariant preserved).
//!
//! Tier ordering (lower number = higher authority):
//! ```text
//! 0 = regulatory
//! 1 = clinical
//! 2 = observational
//! 3 = expert / team_policy
//! 4 = community
//! 5 = anecdotal
//! ```
//!
//! A promotion is only valid when `target_tier_number < current_tier_number`.

use serde::{Deserialize, Serialize};

/// Request to promote a claim to a higher authority tier.
///
/// Promotion creates a new claim that supersedes the original (append-only).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromotionRequest {
    /// ID of the claim to promote.
    pub claim_id: String,

    /// Target tier name (must represent higher authority than current tier).
    ///
    /// Accepted values: `"regulatory"`, `"clinical"`, `"observational"`,
    /// `"team_policy"`, `"expert"`, `"community"`, `"anecdotal"`.
    pub target_tier: String,

    /// Evidence supporting the higher tier (at least one entry required).
    pub evidence: Vec<String>,

    /// Human-readable justification for the promotion.
    pub reason: String,

    /// Identity of the person or agent performing the promotion.
    pub promoted_by: String,
}

/// Result of a promotion operation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromotionResult {
    /// The original claim ID that was promoted.
    pub original_claim_id: String,

    /// The new claim ID created to supersede the original.
    ///
    /// Only meaningful when `success == true`.
    pub new_claim_id: String,

    /// Human-readable name of the previous tier.
    pub previous_tier: String,

    /// Human-readable name of the new tier.
    pub new_tier: String,

    /// Whether the promotion succeeded.
    pub success: bool,

    /// Error message when `success == false`.
    pub error: Option<String>,
}

/// Validation error for a promotion request.
///
/// Returned before any write is attempted, so no state has changed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PromotionValidationError {
    /// Target tier has equal or lower authority than the current tier.
    TierNotHigher {
        /// Current tier number.
        current: u8,
        /// Requested target tier number.
        requested: u8,
    },
    /// No evidence provided; at least one reference is required for promotion.
    MissingEvidence,
    /// The `reason` field is empty or whitespace-only.
    MissingReason,
    /// The claim to be promoted was not found.
    ClaimNotFound(String),
    /// The claim is deprecated and cannot be promoted.
    ClaimDeprecated,
}

impl std::fmt::Display for PromotionValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::TierNotHigher { current, requested } => write!(
                f,
                "target tier {} has equal or lower authority than current tier {} \
                 (lower number = higher authority)",
                requested, current
            ),
            Self::MissingEvidence => {
                write!(f, "promotion requires at least one evidence reference")
            }
            Self::MissingReason => write!(f, "promotion requires a non-empty reason"),
            Self::ClaimNotFound(id) => write!(f, "claim '{}' not found", id),
            Self::ClaimDeprecated => write!(
                f,
                "cannot promote a deprecated claim; supersede it with a new active claim first"
            ),
        }
    }
}

/// Convert a tier name string to its integer tier number.
///
/// Returns `None` for unknown tier names.
/// `team_policy` maps to 3 for integer comparison (same as `expert`).
pub fn tier_name_to_number(tier: &str) -> Option<u8> {
    match tier.to_lowercase().as_str() {
        "regulatory" => Some(0),
        "clinical" => Some(1),
        "observational" => Some(2),
        "team_policy" | "expert" => Some(3),
        "community" => Some(4),
        "anecdotal" => Some(5),
        _ => None,
    }
}

/// Validate a promotion request before any write occurs.
///
/// Returns `Ok(())` when the request is valid. Returns the first validation
/// error found, checking: deprecated → evidence → reason → tier.
pub fn validate_promotion(
    request: &PromotionRequest,
    current_tier: &str,
    is_deprecated: bool,
) -> Result<(), PromotionValidationError> {
    if is_deprecated {
        return Err(PromotionValidationError::ClaimDeprecated);
    }
    if request.evidence.is_empty() {
        return Err(PromotionValidationError::MissingEvidence);
    }
    if request.reason.trim().is_empty() {
        return Err(PromotionValidationError::MissingReason);
    }
    let current_num = tier_name_to_number(current_tier).unwrap_or(5);
    let target_num = match tier_name_to_number(&request.target_tier) {
        Some(n) => n,
        None => {
            return Err(PromotionValidationError::TierNotHigher {
                current: current_num,
                requested: 5,
            });
        }
    };
    if target_num >= current_num {
        return Err(PromotionValidationError::TierNotHigher {
            current: current_num,
            requested: target_num,
        });
    }
    Ok(())
}

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

    #[test]
    fn test_tier_name_to_number_known_tiers() {
        assert_eq!(tier_name_to_number("regulatory"), Some(0));
        assert_eq!(tier_name_to_number("clinical"), Some(1));
        assert_eq!(tier_name_to_number("observational"), Some(2));
        assert_eq!(tier_name_to_number("team_policy"), Some(3));
        assert_eq!(tier_name_to_number("expert"), Some(3));
        assert_eq!(tier_name_to_number("community"), Some(4));
        assert_eq!(tier_name_to_number("anecdotal"), Some(5));
    }

    #[test]
    fn test_tier_name_to_number_case_insensitive() {
        assert_eq!(tier_name_to_number("Regulatory"), Some(0));
        assert_eq!(tier_name_to_number("CLINICAL"), Some(1));
        assert_eq!(tier_name_to_number("Expert"), Some(3));
    }

    #[test]
    fn test_tier_name_to_number_unknown() {
        assert_eq!(tier_name_to_number(""), None);
        assert_eq!(tier_name_to_number("tier3"), None);
    }

    #[test]
    fn test_error_display_tier_not_higher() {
        let err = PromotionValidationError::TierNotHigher { current: 3, requested: 4 };
        let msg = err.to_string();
        assert!(msg.contains('4'));
        assert!(msg.contains('3'));
        assert!(msg.contains("lower authority"));
    }

    #[test]
    fn test_error_display_missing_evidence() {
        assert!(PromotionValidationError::MissingEvidence.to_string().contains("evidence"));
    }

    #[test]
    fn test_error_display_claim_not_found() {
        let err = PromotionValidationError::ClaimNotFound("my-claim-001".to_string());
        assert!(err.to_string().contains("my-claim-001"));
    }

    fn valid_request() -> PromotionRequest {
        PromotionRequest {
            claim_id: "test-001".to_string(),
            target_tier: "expert".to_string(),
            evidence: vec!["ADR-007".to_string()],
            reason: "Adopted as org-wide standard".to_string(),
            promoted_by: "jml".to_string(),
        }
    }

    #[test]
    fn test_validate_promotion_happy_path() {
        let req = PromotionRequest { target_tier: "expert".to_string(), ..valid_request() };
        assert!(validate_promotion(&req, "community", false).is_ok());
    }

    #[test]
    fn test_validate_promotion_rejected_deprecated() {
        let req = valid_request();
        assert_eq!(
            validate_promotion(&req, "community", true),
            Err(PromotionValidationError::ClaimDeprecated)
        );
    }

    #[test]
    fn test_validate_promotion_rejected_missing_evidence() {
        let req = PromotionRequest { evidence: vec![], ..valid_request() };
        assert_eq!(
            validate_promotion(&req, "community", false),
            Err(PromotionValidationError::MissingEvidence)
        );
    }

    #[test]
    fn test_validate_promotion_rejected_empty_reason() {
        let req = PromotionRequest { reason: "   ".to_string(), ..valid_request() };
        assert_eq!(
            validate_promotion(&req, "community", false),
            Err(PromotionValidationError::MissingReason)
        );
    }

    #[test]
    fn test_validate_promotion_rejected_same_tier() {
        let req = PromotionRequest { target_tier: "expert".to_string(), ..valid_request() };
        assert_eq!(
            validate_promotion(&req, "expert", false),
            Err(PromotionValidationError::TierNotHigher { current: 3, requested: 3 })
        );
    }

    #[test]
    fn test_validate_promotion_rejected_lower_tier() {
        let req = PromotionRequest { target_tier: "clinical".to_string(), ..valid_request() };
        assert_eq!(
            validate_promotion(&req, "regulatory", false),
            Err(PromotionValidationError::TierNotHigher { current: 0, requested: 1 })
        );
    }

    #[test]
    fn test_validate_promotion_anecdotal_to_regulatory() {
        let req = PromotionRequest { target_tier: "regulatory".to_string(), ..valid_request() };
        assert!(validate_promotion(&req, "anecdotal", false).is_ok());
    }

    #[test]
    fn test_validate_promotion_unknown_target_tier() {
        let req = PromotionRequest { target_tier: "mythical".to_string(), ..valid_request() };
        assert!(matches!(
            validate_promotion(&req, "community", false),
            Err(PromotionValidationError::TierNotHigher { .. })
        ));
    }

    #[test]
    fn test_promotion_result_serde_roundtrip() {
        let result = PromotionResult {
            original_claim_id: "test-001".to_string(),
            new_claim_id: "test-002".to_string(),
            previous_tier: "community".to_string(),
            new_tier: "expert".to_string(),
            success: true,
            error: None,
        };
        let json = serde_json::to_string(&result).expect("serialization failed");
        let restored: PromotionResult =
            serde_json::from_str(&json).expect("deserialization failed");
        assert!(restored.success);
        assert!(restored.error.is_none());
    }
}
