use crate::config::enhanced_models::{
EnhancedScoringProfile, GlobalBonus, GlobalPenalty, MetricOverride,
};
use crate::metrics::definitions::{ThresholdSet, METRIC_REGISTRY};
use crate::AnalyzeError; use std::collections::{HashMap, HashSet};
use std::sync::Arc;
pub type ModifierResult<T> = Result<T, AnalyzeError>;
#[derive(Debug, Clone)]
pub struct ProfileModifier {
base_profile: Arc<EnhancedScoringProfile>,
modifications: ProfileModifications,
}
#[derive(Debug, Clone, Default)]
pub struct ProfileModifications {
pub metric_overrides: HashMap<String, MetricOverrideModification>,
pub custom_penalties: Vec<GlobalPenalty>,
pub custom_bonuses: Vec<GlobalBonus>,
pub disabled_metrics: HashSet<String>,
}
#[derive(Debug, Clone, Default)]
pub struct MetricOverrideModification {
pub weight: Option<f32>,
pub enabled: Option<bool>,
pub thresholds: Option<Vec<f32>>,
pub penalty_multiplier: Option<f32>,
}
impl ProfileModifier {
pub fn new(base_profile: Arc<EnhancedScoringProfile>) -> Self {
Self {
base_profile,
modifications: ProfileModifications::default(),
}
}
pub fn apply_modifications(&self) -> EnhancedScoringProfile {
let mut modified = (*self.base_profile).clone();
for (metric_name, override_mod) in &self.modifications.metric_overrides {
let metric_override = modified
.metric_overrides
.entry(metric_name.clone())
.or_insert_with(MetricOverride::default);
if let Some(weight) = override_mod.weight {
metric_override.weight = weight;
}
if let Some(enabled) = override_mod.enabled {
metric_override.enabled = enabled;
}
if let Some(ref thresholds) = override_mod.thresholds {
if thresholds.len() == 4 {
metric_override.thresholds = Some(ThresholdSet {
poor: thresholds[0], fair: thresholds[1], good: thresholds[2], excellent: thresholds[3], });
} else if thresholds.len() >= 4 {
metric_override.thresholds = Some(ThresholdSet {
poor: thresholds[0],
fair: thresholds[1],
good: thresholds[2],
excellent: thresholds[3],
});
}
}
if let Some(penalty_multiplier) = override_mod.penalty_multiplier {
metric_override.penalty_multiplier = penalty_multiplier;
}
}
for metric_name in &self.modifications.disabled_metrics {
let metric_override = modified
.metric_overrides
.entry(metric_name.clone())
.or_insert_with(MetricOverride::default);
metric_override.enabled = false;
}
for (idx, penalty) in self.modifications.custom_penalties.iter().enumerate() {
let key = format!("custom_penalty_{}", idx);
modified
.penalties
.severe_penalties
.insert(key, penalty.clone());
}
for (idx, bonus) in self.modifications.custom_bonuses.iter().enumerate() {
let key = format!("custom_bonus_{}", idx);
modified
.bonuses
.excellence_bonuses
.insert(key, bonus.clone());
}
modified
}
pub fn enable_metric(&mut self, metric_name: &str) -> ModifierResult<&mut Self> {
self.validate_metric_name(metric_name)?;
self.modifications.disabled_metrics.remove(metric_name);
let override_mod = self
.modifications
.metric_overrides
.entry(metric_name.to_string())
.or_insert_with(MetricOverrideModification::default);
override_mod.enabled = Some(true);
Ok(self)
}
pub fn disable_metric(&mut self, metric_name: &str) -> ModifierResult<&mut Self> {
self.validate_metric_name(metric_name)?;
self.modifications
.disabled_metrics
.insert(metric_name.to_string());
let override_mod = self
.modifications
.metric_overrides
.entry(metric_name.to_string())
.or_insert_with(MetricOverrideModification::default);
override_mod.enabled = Some(false);
Ok(self)
}
pub fn set_threshold(
&mut self,
metric_name: &str,
thresholds: Vec<f32>,
) -> ModifierResult<&mut Self> {
self.validate_metric_name(metric_name)?;
self.validate_threshold_vec(&thresholds)?;
let override_mod = self
.modifications
.metric_overrides
.entry(metric_name.to_string())
.or_insert_with(MetricOverrideModification::default);
override_mod.thresholds = Some(thresholds);
Ok(self)
}
pub fn set_weight(&mut self, metric_name: &str, weight: f32) -> ModifierResult<&mut Self> {
self.validate_metric_name(metric_name)?;
self.validate_weight(weight)?;
let override_mod = self
.modifications
.metric_overrides
.entry(metric_name.to_string())
.or_insert_with(MetricOverrideModification::default);
override_mod.weight = Some(weight);
Ok(self)
}
pub fn set_penalty_multiplier(
&mut self,
metric_name: &str,
multiplier: f32,
) -> ModifierResult<&mut Self> {
self.validate_metric_name(metric_name)?;
self.validate_penalty_multiplier(multiplier)?;
let override_mod = self
.modifications
.metric_overrides
.entry(metric_name.to_string())
.or_insert_with(MetricOverrideModification::default);
override_mod.penalty_multiplier = Some(multiplier);
Ok(self)
}
pub fn add_penalty(&mut self, penalty: GlobalPenalty) -> &mut Self {
self.modifications.custom_penalties.push(penalty);
self
}
pub fn add_bonus(&mut self, bonus: GlobalBonus) -> &mut Self {
self.modifications.custom_bonuses.push(bonus);
self
}
pub fn base_profile(&self) -> &EnhancedScoringProfile {
&self.base_profile
}
pub fn modifications(&self) -> &ProfileModifications {
&self.modifications
}
pub fn is_metric_disabled(&self, metric_name: &str) -> bool {
self.modifications.disabled_metrics.contains(metric_name)
|| self
.modifications
.metric_overrides
.get(metric_name)
.and_then(|m| m.enabled)
== Some(false)
}
pub fn clear_modifications(&mut self) -> &mut Self {
self.modifications = ProfileModifications::default();
self
}
fn validate_metric_name(&self, metric_name: &str) -> ModifierResult<()> {
if METRIC_REGISTRY.get_metric_definition(metric_name).is_none() {
return Err(AnalyzeError::InvalidMetricName(format!(
"Unknown metric: '{}'. Check METRIC_REGISTRY for valid metric names.",
metric_name
)));
}
Ok(())
}
fn validate_threshold_vec(&self, thresholds: &[f32]) -> ModifierResult<()> {
if thresholds.len() != 4 {
return Err(AnalyzeError::InvalidThreshold(format!(
"Threshold vector must have exactly 4 values [min, optimal_min, optimal_max, max], got {}",
thresholds.len()
)));
}
let min = thresholds[0];
let optimal_min = thresholds[1];
let optimal_max = thresholds[2];
let max = thresholds[3];
if !(min < optimal_min) {
return Err(AnalyzeError::InvalidThreshold(format!(
"min ({}) must be < optimal_min ({})",
min, optimal_min
)));
}
if !(optimal_min <= optimal_max) {
return Err(AnalyzeError::InvalidThreshold(format!(
"optimal_min ({}) must be <= optimal_max ({})",
optimal_min, optimal_max
)));
}
if !(optimal_max < max) {
return Err(AnalyzeError::InvalidThreshold(format!(
"optimal_max ({}) must be < max ({})",
optimal_max, max
)));
}
for (i, &val) in thresholds.iter().enumerate() {
if !val.is_finite() {
let names = ["min", "optimal_min", "optimal_max", "max"];
return Err(AnalyzeError::InvalidThreshold(format!(
"{} value must be finite (got {})",
names[i], val
)));
}
if val < 0.0 {
let names = ["min", "optimal_min", "optimal_max", "max"];
return Err(AnalyzeError::InvalidThreshold(format!(
"{} value cannot be negative (got {})",
names[i], val
)));
}
}
Ok(())
}
fn validate_thresholds(&self, thresholds: &ThresholdSet) -> ModifierResult<()> {
if thresholds.poor > thresholds.fair {
return Err(AnalyzeError::InvalidThreshold(format!(
"poor ({}) must be <= fair ({})",
thresholds.poor, thresholds.fair
)));
}
if thresholds.fair > thresholds.good {
return Err(AnalyzeError::InvalidThreshold(format!(
"fair ({}) must be <= good ({})",
thresholds.fair, thresholds.good
)));
}
if thresholds.good > thresholds.excellent {
return Err(AnalyzeError::InvalidThreshold(format!(
"good ({}) must be <= excellent ({})",
thresholds.good, thresholds.excellent
)));
}
Ok(())
}
fn validate_weight(&self, weight: f32) -> ModifierResult<()> {
if !weight.is_finite() {
return Err(AnalyzeError::InvalidWeight(format!(
"Weight must be finite, got {}",
weight
)));
}
if weight < 0.0 || weight > 10.0 {
return Err(AnalyzeError::InvalidWeight(format!(
"Weight must be between 0.0 and 10.0, got {}",
weight
)));
}
Ok(())
}
fn validate_penalty_multiplier(&self, multiplier: f32) -> ModifierResult<()> {
if multiplier < 0.0 || multiplier > 10.0 {
return Err(AnalyzeError::InvalidPenaltyMultiplier(format!(
"Penalty multiplier must be between 0.0 and 10.0, got {}",
multiplier
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::enhanced_models::{
BonusTrigger, GlobalBonus, GlobalPenalty, PenaltyTrigger, PenaltyType,
};
fn create_test_profile() -> Arc<EnhancedScoringProfile> {
Arc::new(EnhancedScoringProfile::default_with_name("test_profile"))
}
#[test]
fn test_new_profile_modifier() {
let profile = create_test_profile();
let modifier = ProfileModifier::new(profile.clone());
assert_eq!(modifier.base_profile().metadata.name, profile.metadata.name);
assert!(modifier.modifications().metric_overrides.is_empty());
assert!(modifier.modifications().custom_penalties.is_empty());
assert!(modifier.modifications().custom_bonuses.is_empty());
}
#[test]
fn test_enable_metric() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
let result = modifier.enable_metric("word_count");
assert!(result.is_ok());
assert!(!modifier.is_metric_disabled("word_count"));
}
#[test]
fn test_disable_metric() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
let result = modifier.disable_metric("word_count");
assert!(result.is_ok());
assert!(modifier.is_metric_disabled("word_count"));
}
#[test]
fn test_invalid_metric_name() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
let result = modifier.enable_metric("nonexistent_metric");
assert!(result.is_err());
}
#[test]
fn test_set_weight() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
let result = modifier.set_weight("word_count", 2.5);
assert!(result.is_ok());
let result = modifier.set_weight("word_count", 15.0);
assert!(result.is_err());
let result = modifier.set_weight("word_count", -1.0);
assert!(result.is_err());
}
#[test]
fn test_set_threshold() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
let thresholds = vec![100.0, 500.0, 2000.0, 5000.0];
let result = modifier.set_threshold("word_count", thresholds);
assert!(result.is_ok());
}
#[test]
fn test_invalid_threshold_ordering() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
let thresholds = vec![500.0, 100.0, 2000.0, 5000.0];
let result = modifier.set_threshold("word_count", thresholds);
assert!(result.is_err());
let thresholds = vec![100.0, 500.0, 5000.0, 2000.0];
let result = modifier.set_threshold("word_count", thresholds);
assert!(result.is_err());
let thresholds = vec![100.0, 500.0];
let result = modifier.set_threshold("word_count", thresholds);
assert!(result.is_err());
}
#[test]
fn test_add_penalty() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
let penalty = GlobalPenalty {
trigger_condition: PenaltyTrigger::MetricBelow {
metric: "word_count".to_string(),
threshold: 100.0,
},
penalty_type: PenaltyType::FixedPoints { points: 5.0 },
description: "Test penalty".to_string(),
enabled: true,
};
modifier.add_penalty(penalty);
assert_eq!(modifier.modifications().custom_penalties.len(), 1);
}
#[test]
fn test_add_bonus() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
let bonus = GlobalBonus {
trigger_condition: BonusTrigger::MetricExcellence {
metric: "word_count".to_string(),
threshold: 1000.0,
},
bonus_points: 5.0,
description: "Test bonus".to_string(),
enabled: true,
};
modifier.add_bonus(bonus);
assert_eq!(modifier.modifications().custom_bonuses.len(), 1);
}
#[test]
fn test_apply_modifications() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
modifier.enable_metric("word_count").unwrap();
modifier.set_weight("word_count", 2.0).unwrap();
modifier.disable_metric("title_len").unwrap();
let modified_profile = modifier.apply_modifications();
assert!(modified_profile.metric_overrides.contains_key("word_count"));
assert_eq!(
modified_profile
.metric_overrides
.get("word_count")
.unwrap()
.weight,
2.0
);
assert!(modified_profile.metric_overrides.contains_key("title_len"));
assert_eq!(
modified_profile
.metric_overrides
.get("title_len")
.unwrap()
.enabled,
false
);
}
#[test]
fn test_clear_modifications() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
modifier.enable_metric("word_count").unwrap();
modifier.set_weight("word_count", 2.0).unwrap();
modifier.clear_modifications();
assert!(modifier.modifications().metric_overrides.is_empty());
assert!(modifier.modifications().custom_penalties.is_empty());
assert!(modifier.modifications().custom_bonuses.is_empty());
}
#[test]
fn test_method_chaining() {
let profile = create_test_profile();
let mut modifier = ProfileModifier::new(profile);
let result = modifier
.enable_metric("word_count")
.and_then(|m| m.set_weight("word_count", 2.0))
.and_then(|m| {
m.set_threshold(
"word_count",
vec![50.0, 100.0, 500.0, 1000.0], )
});
assert!(result.is_ok());
}
}