//! Diversity constraint types and result structures.
//!
//! Defines the constraint parameters that the diversity selector enforces,
//! the violation type for reporting breaches, and the result wrapper returned
//! from selection.

use super::super::executor::ScoredCandidate;

// -- Constraints ------------------------------------------------------------

/// Builder-style diversity constraints applied after scoring.
///
/// Only `max_per_creator` and `format_mix_max_fraction` are enforced in M2.
/// The remaining fields (`min_exploration`, `topic_diversity`, `category_min`)
/// are reserved for M3/M6 and ignored by the selector.
#[derive(Debug, Clone, Default)]
pub struct DiversityConstraints {
    pub max_per_creator: Option<usize>,
    pub format_mix_max_fraction: Option<f64>,
    /// Reserved for M3: minimum fraction of exploration candidates.
    pub min_exploration: Option<f64>,
    /// Reserved for M6: topic diversity enforcement.
    pub topic_diversity: Option<f64>,
    /// Reserved for M6: minimum number of distinct categories.
    pub category_min: Option<usize>,
}

impl DiversityConstraints {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub const fn max_per_creator(mut self, n: usize) -> Self {
        self.max_per_creator = Some(n);
        self
    }

    #[must_use]
    pub const fn format_mix(mut self, max_fraction: f64) -> Self {
        self.format_mix_max_fraction = Some(max_fraction);
        self
    }
}

// -- Violation --------------------------------------------------------------

/// A single diversity constraint violation, describing which constraint was
/// breached and the specifics.
#[derive(Debug, Clone)]
pub struct ConstraintViolation {
    /// The constraint name, e.g. `"max_per_creator"` or `"format_mix"`.
    pub constraint: String,
    /// Human-readable description of the violation.
    pub detail: String,
}

// -- Result -----------------------------------------------------------------

/// Result of diversity selection, including the filtered candidate list and
/// any constraint violations that could not be resolved.
pub struct DiversityResult {
    pub selected: Vec<ScoredCandidate>,
    pub constraints_satisfied: bool,
    pub violations: Vec<ConstraintViolation>,
}

// -- Tests ------------------------------------------------------------------

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

    #[test]
    fn default_constraints_have_no_active_fields() {
        let c = DiversityConstraints::new();
        assert!(c.max_per_creator.is_none());
        assert!(c.format_mix_max_fraction.is_none());
        assert!(c.min_exploration.is_none());
        assert!(c.topic_diversity.is_none());
        assert!(c.category_min.is_none());
    }

    #[test]
    fn builder_sets_max_per_creator() {
        let c = DiversityConstraints::new().max_per_creator(3);
        assert_eq!(c.max_per_creator, Some(3));
    }

    #[test]
    fn builder_sets_format_mix() {
        let c = DiversityConstraints::new().format_mix(0.5);
        assert_eq!(c.format_mix_max_fraction, Some(0.5));
    }

    #[test]
    fn builder_chains_both_constraints() {
        let c = DiversityConstraints::new()
            .max_per_creator(2)
            .format_mix(0.4);
        assert_eq!(c.max_per_creator, Some(2));
        assert_eq!(c.format_mix_max_fraction, Some(0.4));
    }
}
