//! MOEA/D (Multi-objective Evolutionary Algorithm based on Decomposition)
//!
//! Decomposes a multi-objective optimization problem into a set of scalar
//! optimization subproblems using weight vectors and solves them simultaneously
//! in a collaborative manner through neighborhood relationships.
//!
//! Key features:
//! - Uniform weight vector generation for decomposition
//! - Tchebycheff and weighted-sum scalarization approaches
//! - Neighborhood-based mating and replacement
//! - Differential evolution operators for offspring generation
//! - Adaptive neighborhood size
//!
//! # References
//!
//! - Zhang & Li, "MOEA/D: A Multiobjective Evolutionary Algorithm Based on
//!   Decomposition", IEEE TEC 2007

use super::{utils, MultiObjectiveConfig, MultiObjectiveOptimizer};
use crate::error::OptimizeError;
use crate::multi_objective::solutions::{MultiObjectiveResult, MultiObjectiveSolution, Population};
use scirs2_core::ndarray::{Array1, ArrayView1};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{Rng, RngExt, SeedableRng};

/// Scalarization approach for MOEA/D
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScalarizationMethod {
    /// Tchebycheff approach: min max_j { w_j * |f_j(x) - z_j*| }
    Tchebycheff,
    /// Weighted sum: min sum_j { w_j * f_j(x) }
    WeightedSum,
    /// Penalty-based Boundary Intersection (PBI)
    PBI,
}

impl Default for ScalarizationMethod {
    fn default() -> Self {
        ScalarizationMethod::Tchebycheff
    }
}

/// MOEA/D optimizer
pub struct MOEAD {
    config: MultiObjectiveConfig,
    n_objectives: usize,
    n_variables: usize,
    /// Weight vectors for decomposition
    weight_vectors: Vec<Array1<f64>>,
    /// Neighborhood indices for each subproblem
    neighborhoods: Vec<Vec<usize>>,
    /// Neighborhood size
    neighborhood_size: usize,
    /// Current solutions (one per subproblem)
    solutions: Vec<MultiObjectiveSolution>,
    /// Ideal point z* (best value per objective)
    ideal_point: Array1<f64>,
    /// Scalarization method
    scalarization: ScalarizationMethod,
    population: Population,
    generation: usize,
    n_evaluations: usize,
    rng: StdRng,
    /// Crossover rate for DE operator
    cr: f64,
    /// Scaling factor for DE operator
    f_scale: f64,
    /// Probability of selecting from neighborhood vs whole population
    delta: f64,
    /// Maximum number of replacements per offspring
    nr: usize,
    convergence_history: Vec<f64>,
}

impl MOEAD {
    /// Create new MOEA/D optimizer
    pub fn new(population_size: usize, n_objectives: usize, n_variables: usize) -> Self {
        let config = MultiObjectiveConfig {
            population_size,
            ..Default::default()
        };
        Self::with_config(
            config,
            n_objectives,
            n_variables,
            ScalarizationMethod::Tchebycheff,
        )
    }

    /// Create MOEA/D with full configuration
    pub fn with_config(
        config: MultiObjectiveConfig,
        n_objectives: usize,
        n_variables: usize,
        scalarization: ScalarizationMethod,
    ) -> Self {
        let neighborhood_size = config.neighborhood_size.unwrap_or(20);
        let neighborhood_size = neighborhood_size.min(config.population_size);

        let seed = config.random_seed.unwrap_or_else(|| {
            use std::time::{SystemTime, UNIX_EPOCH};
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .map(|d| d.as_secs())
                .unwrap_or(42)
        });

        let rng = StdRng::seed_from_u64(seed);

        // Generate weight vectors
        let weight_vectors = generate_uniform_weight_vectors(n_objectives, config.population_size);

        let actual_pop_size = weight_vectors.len();

        // Compute neighborhoods based on Euclidean distance between weight vectors
        let neighborhoods = compute_neighborhoods(&weight_vectors, neighborhood_size);

        Self {
            config: MultiObjectiveConfig {
                population_size: actual_pop_size,
                ..config
            },
            n_objectives,
            n_variables,
            weight_vectors,
            neighborhoods,
            neighborhood_size,
            solutions: Vec::new(),
            ideal_point: Array1::from_elem(n_objectives, f64::INFINITY),
            scalarization,
            population: Population::new(),
            generation: 0,
            n_evaluations: 0,
            rng,
            cr: 1.0,      // Crossover rate
            f_scale: 0.5, // DE scaling factor
            delta: 0.9,   // Probability of neighbor selection
            nr: 2,        // Max replacements
            convergence_history: Vec::new(),
        }
    }

    /// Evaluate a single individual
    fn evaluate_individual<F>(
        &mut self,
        variables: &Array1<f64>,
        objective_function: &F,
    ) -> Result<Array1<f64>, OptimizeError>
    where
        F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync,
    {
        self.n_evaluations += 1;

        if let Some(max_evals) = self.config.max_evaluations {
            if self.n_evaluations > max_evals {
                return Err(OptimizeError::MaxEvaluationsReached);
            }
        }

        let objectives = objective_function(&variables.view());
        if objectives.len() != self.n_objectives {
            return Err(OptimizeError::InvalidInput(format!(
                "Expected {} objectives, got {}",
                self.n_objectives,
                objectives.len()
            )));
        }

        Ok(objectives)
    }

    /// Update ideal point
    fn update_ideal_point(&mut self, objectives: &Array1<f64>) {
        for (i, &obj) in objectives.iter().enumerate() {
            if obj < self.ideal_point[i] {
                self.ideal_point[i] = obj;
            }
        }
    }

    /// Scalarizing function value for a given solution and weight vector
    fn scalar_value(&self, objectives: &Array1<f64>, weight: &Array1<f64>) -> f64 {
        match self.scalarization {
            ScalarizationMethod::Tchebycheff => {
                // max_j { w_j * |f_j(x) - z_j*| }
                objectives
                    .iter()
                    .zip(weight.iter())
                    .zip(self.ideal_point.iter())
                    .map(|((obj, w), ideal)| {
                        let w_eff = if *w < 1e-10 { 1e-10 } else { *w };
                        w_eff * (obj - ideal).abs()
                    })
                    .fold(0.0_f64, f64::max)
            }
            ScalarizationMethod::WeightedSum => {
                // sum_j { w_j * f_j(x) }
                objectives
                    .iter()
                    .zip(weight.iter())
                    .map(|(obj, w)| w * obj)
                    .sum()
            }
            ScalarizationMethod::PBI => {
                // PBI: d1 + theta * d2
                // d1 = ||(f - z*) . w|| / ||w||
                // d2 = ||f - z* - d1 * w / ||w|| ||
                let theta = 5.0; // penalty parameter

                let diff: Array1<f64> = objectives - &self.ideal_point;
                let w_norm = weight.dot(weight).sqrt();
                if w_norm < 1e-30 {
                    return diff.dot(&diff).sqrt();
                }

                let w_unit = weight / w_norm;
                let d1 = diff.dot(&w_unit).abs();
                let proj = d1 * &w_unit;
                let perp = &diff - &proj;
                let d2 = perp.dot(&perp).sqrt();

                d1 + theta * d2
            }
        }
    }

    /// Generate offspring using DE/rand/1 operator within the neighborhood
    fn generate_offspring(&mut self, subproblem_idx: usize) -> Array1<f64> {
        let pool = if self.rng.random::<f64>() < self.delta {
            // Select from neighborhood
            &self.neighborhoods[subproblem_idx]
        } else {
            // Select from whole population (use neighborhood as fallback)
            &self.neighborhoods[subproblem_idx]
        };

        if pool.len() < 3 || self.solutions.len() < 3 {
            // Fallback: random perturbation of current solution
            let current = &self.solutions[subproblem_idx].variables;
            let mut child = current.clone();
            let bounds: Vec<(f64, f64)> = if let Some((lower, upper)) = &self.config.bounds {
                lower
                    .iter()
                    .zip(upper.iter())
                    .map(|(&l, &u)| (l, u))
                    .collect()
            } else {
                vec![(-1.0, 1.0); self.n_variables]
            };

            for j in 0..self.n_variables {
                if self.rng.random::<f64>() < 0.1 {
                    let (lb, ub) = bounds[j];
                    child[j] = self.rng.random_range(lb..ub);
                }
            }
            return child;
        }

        // Select 3 distinct indices from pool
        let mut indices = Vec::with_capacity(3);
        let mut attempts = 0;
        while indices.len() < 3 && attempts < 100 {
            let idx = pool[self.rng.random_range(0..pool.len())];
            if idx < self.solutions.len() && !indices.contains(&idx) && idx != subproblem_idx {
                indices.push(idx);
            }
            attempts += 1;
        }

        // If we couldn't get 3 distinct indices, use simple mutation
        if indices.len() < 3 {
            let current = &self.solutions[subproblem_idx].variables;
            return current.clone();
        }

        // DE/rand/1
        let x1 = &self.solutions[indices[0]].variables;
        let x2 = &self.solutions[indices[1]].variables;
        let x3 = &self.solutions[indices[2]].variables;

        let current = &self.solutions[subproblem_idx].variables;
        let mut child = Array1::zeros(self.n_variables);

        let j_rand = self.rng.random_range(0..self.n_variables);
        for j in 0..self.n_variables {
            if self.rng.random::<f64>() < self.cr || j == j_rand {
                child[j] = x1[j] + self.f_scale * (x2[j] - x3[j]);
            } else {
                child[j] = current[j];
            }
        }

        // Apply bounds
        if let Some((lower, upper)) = &self.config.bounds {
            for j in 0..self.n_variables {
                child[j] = child[j].max(lower[j]).min(upper[j]);
            }
        }

        child
    }

    /// Update neighboring solutions with offspring if it improves their subproblem
    fn update_neighbors(&mut self, subproblem_idx: usize, offspring: &MultiObjectiveSolution) {
        let neighbors = self.neighborhoods[subproblem_idx].clone();
        let mut count = 0;

        for &neighbor_idx in &neighbors {
            if count >= self.nr {
                break;
            }
            if neighbor_idx >= self.solutions.len() || neighbor_idx >= self.weight_vectors.len() {
                continue;
            }

            let w = &self.weight_vectors[neighbor_idx];
            let new_scalar = self.scalar_value(&offspring.objectives, w);
            let old_scalar = self.scalar_value(&self.solutions[neighbor_idx].objectives, w);

            if new_scalar < old_scalar {
                self.solutions[neighbor_idx] = offspring.clone();
                count += 1;
            }
        }
    }

    /// Calculate metrics
    fn calculate_metrics(&mut self) {
        if let Some(ref_point) = &self.config.reference_point {
            let pop = Population::from_solutions(self.solutions.clone());
            let pareto_front = pop.extract_pareto_front();
            let hv = utils::calculate_hypervolume(&pareto_front, ref_point);
            self.convergence_history.push(hv);
        }
    }
}

impl MultiObjectiveOptimizer for MOEAD {
    fn optimize<F>(&mut self, objective_function: F) -> Result<MultiObjectiveResult, OptimizeError>
    where
        F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync,
    {
        self.initialize_population()?;

        // Generate and evaluate initial population
        let pop_size = self.weight_vectors.len();
        let initial_vars =
            utils::generate_random_population(pop_size, self.n_variables, &self.config.bounds);

        self.solutions.clear();
        for vars in initial_vars {
            let objs = self.evaluate_individual(&vars, &objective_function)?;
            self.update_ideal_point(&objs);
            self.solutions.push(MultiObjectiveSolution::new(vars, objs));
        }

        // Main loop
        while self.generation < self.config.max_generations {
            if self.check_convergence() {
                break;
            }
            self.evolve_generation(&objective_function)?;
        }

        // Build results
        let pop = Population::from_solutions(self.solutions.clone());
        let pareto_front = pop.extract_pareto_front();
        let hypervolume = self
            .config
            .reference_point
            .as_ref()
            .map(|rp| utils::calculate_hypervolume(&pareto_front, rp));

        let mut result = MultiObjectiveResult::new(
            pareto_front,
            self.solutions.clone(),
            self.n_evaluations,
            self.generation,
        );
        result.hypervolume = hypervolume;
        result.metrics.convergence_history = self.convergence_history.clone();

        Ok(result)
    }

    fn evolve_generation<F>(&mut self, objective_function: &F) -> Result<(), OptimizeError>
    where
        F: Fn(&ArrayView1<f64>) -> Array1<f64> + Send + Sync,
    {
        let pop_size = self.solutions.len();

        for i in 0..pop_size {
            // Generate offspring
            let child_vars = self.generate_offspring(i);

            // Evaluate offspring
            let child_objs = self.evaluate_individual(&child_vars, objective_function)?;

            // Update ideal point
            self.update_ideal_point(&child_objs);

            let offspring = MultiObjectiveSolution::new(child_vars, child_objs);

            // Update neighbors
            self.update_neighbors(i, &offspring);
        }

        self.generation += 1;

        // Update population reference for the trait
        self.population = Population::from_solutions(self.solutions.clone());

        self.calculate_metrics();

        Ok(())
    }

    fn initialize_population(&mut self) -> Result<(), OptimizeError> {
        self.solutions.clear();
        self.population.clear();
        self.generation = 0;
        self.n_evaluations = 0;
        self.ideal_point = Array1::from_elem(self.n_objectives, f64::INFINITY);
        self.convergence_history.clear();
        Ok(())
    }

    fn check_convergence(&self) -> bool {
        if let Some(max_evals) = self.config.max_evaluations {
            if self.n_evaluations >= max_evals {
                return true;
            }
        }

        if self.convergence_history.len() >= 10 {
            let recent = &self.convergence_history[self.convergence_history.len() - 10..];
            let max_hv = recent.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
            let min_hv = recent.iter().fold(f64::INFINITY, |a, &b| a.min(b));
            if (max_hv - min_hv) < self.config.tolerance {
                return true;
            }
        }

        false
    }

    fn get_population(&self) -> &Population {
        &self.population
    }

    fn get_generation(&self) -> usize {
        self.generation
    }

    fn get_evaluations(&self) -> usize {
        self.n_evaluations
    }

    fn name(&self) -> &str {
        "MOEA/D"
    }
}

/// Generate uniform weight vectors for decomposition
/// Uses the simplex-lattice design (Das & Dennis method)
fn generate_uniform_weight_vectors(n_objectives: usize, target_size: usize) -> Vec<Array1<f64>> {
    if n_objectives == 0 {
        return vec![];
    }
    if n_objectives == 1 {
        return vec![Array1::from_elem(1, 1.0); target_size.max(1)];
    }

    // Determine number of divisions to get approximately target_size vectors
    let mut h = 1;
    loop {
        let n_points = binomial_coefficient(h + n_objectives - 1, n_objectives - 1);
        if n_points >= target_size {
            break;
        }
        h += 1;
        if h > 100 {
            break;
        }
    }

    let mut vectors = Vec::new();
    generate_weight_recursive(
        &mut vectors,
        Array1::zeros(n_objectives),
        0,
        n_objectives,
        h,
        h,
    );

    // Normalize
    for v in &mut vectors {
        let sum: f64 = v.sum();
        if sum > 0.0 {
            *v /= sum;
        }
    }

    // If we have more vectors than needed, take a subset
    if vectors.len() > target_size {
        vectors.truncate(target_size);
    }

    vectors
}

/// Recursive helper for weight vector generation
fn generate_weight_recursive(
    vectors: &mut Vec<Array1<f64>>,
    mut current: Array1<f64>,
    index: usize,
    n_objectives: usize,
    h: usize,
    remaining: usize,
) {
    if index == n_objectives - 1 {
        current[index] = remaining as f64;
        vectors.push(current);
    } else {
        for i in 0..=remaining {
            current[index] = i as f64;
            generate_weight_recursive(
                vectors,
                current.clone(),
                index + 1,
                n_objectives,
                h,
                remaining - i,
            );
        }
    }
}

/// Compute binomial coefficient C(n, k)
fn binomial_coefficient(n: usize, k: usize) -> usize {
    if k > n {
        return 0;
    }
    if k == 0 || k == n {
        return 1;
    }
    let k = k.min(n - k);
    let mut result = 1usize;
    for i in 0..k {
        result = result.saturating_mul(n - i);
        result /= i + 1;
    }
    result
}

/// Compute neighborhoods based on Euclidean distance between weight vectors
fn compute_neighborhoods(
    weight_vectors: &[Array1<f64>],
    neighborhood_size: usize,
) -> Vec<Vec<usize>> {
    let n = weight_vectors.len();
    let t = neighborhood_size.min(n);

    let mut neighborhoods = Vec::with_capacity(n);

    for i in 0..n {
        // Compute distances to all other weight vectors
        let mut distances: Vec<(usize, f64)> = (0..n)
            .map(|j| {
                let diff = &weight_vectors[i] - &weight_vectors[j];
                let dist = diff.dot(&diff).sqrt();
                (j, dist)
            })
            .collect();

        // Sort by distance
        distances.sort_by(|a, b| a.1.total_cmp(&b.1));

        // Take T closest (including self)
        let neighbors: Vec<usize> = distances.iter().take(t).map(|(idx, _)| *idx).collect();
        neighborhoods.push(neighbors);
    }

    neighborhoods
}

#[cfg(test)]
mod tests {
    use super::*;
    use scirs2_core::ndarray::{array, s};

    fn zdt1(x: &ArrayView1<f64>) -> Array1<f64> {
        let f1 = x[0];
        let g = 1.0 + 9.0 * x.slice(s![1..]).sum() / (x.len() - 1) as f64;
        let f2 = g * (1.0 - (f1 / g).sqrt());
        array![f1, f2]
    }

    #[test]
    fn test_moead_creation() {
        let moead = MOEAD::new(50, 2, 3);
        assert_eq!(moead.n_objectives, 2);
        assert_eq!(moead.n_variables, 3);
        assert!(!moead.weight_vectors.is_empty());
        assert_eq!(moead.generation, 0);
    }

    #[test]
    fn test_moead_with_config() {
        let config = MultiObjectiveConfig {
            population_size: 30,
            neighborhood_size: Some(10),
            max_generations: 10,
            random_seed: Some(42),
            ..Default::default()
        };

        let moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::Tchebycheff);
        assert_eq!(moead.neighborhood_size, 10);
    }

    #[test]
    fn test_moead_optimize_zdt1() {
        let config = MultiObjectiveConfig {
            max_generations: 10,
            population_size: 20,
            bounds: Some((Array1::zeros(3), Array1::ones(3))),
            random_seed: Some(42),
            neighborhood_size: Some(5),
            ..Default::default()
        };

        let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::Tchebycheff);
        let result = moead.optimize(zdt1);

        assert!(result.is_ok());
        let res = result.expect("should succeed");
        assert!(res.success);
        assert!(!res.pareto_front.is_empty());
        assert!(res.n_evaluations > 0);
    }

    #[test]
    fn test_moead_weighted_sum() {
        let config = MultiObjectiveConfig {
            max_generations: 10,
            population_size: 20,
            bounds: Some((Array1::zeros(3), Array1::ones(3))),
            random_seed: Some(42),
            ..Default::default()
        };

        let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::WeightedSum);
        let result = moead.optimize(zdt1);

        assert!(result.is_ok());
        let res = result.expect("should succeed");
        assert!(res.success);
    }

    #[test]
    fn test_moead_pbi() {
        let config = MultiObjectiveConfig {
            max_generations: 10,
            population_size: 20,
            bounds: Some((Array1::zeros(3), Array1::ones(3))),
            random_seed: Some(42),
            ..Default::default()
        };

        let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::PBI);
        let result = moead.optimize(zdt1);

        assert!(result.is_ok());
        let res = result.expect("should succeed");
        assert!(res.success);
    }

    #[test]
    fn test_moead_max_evaluations() {
        let config = MultiObjectiveConfig {
            max_generations: 1000,
            max_evaluations: Some(50),
            population_size: 10,
            bounds: Some((Array1::zeros(3), Array1::ones(3))),
            random_seed: Some(42),
            ..Default::default()
        };

        let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::Tchebycheff);
        let result = moead.optimize(zdt1);
        assert!(result.is_ok());
        let res = result.expect("should succeed");
        assert!(res.n_evaluations <= 70); // Allow some overshoot
    }

    #[test]
    fn test_weight_vector_generation() {
        let vectors = generate_uniform_weight_vectors(2, 10);
        assert!(!vectors.is_empty());

        // All weights should sum to 1
        for v in &vectors {
            let sum: f64 = v.sum();
            assert!(
                (sum - 1.0).abs() < 1e-10,
                "Weight sum should be 1.0, got {}",
                sum
            );
        }

        // All components should be non-negative
        for v in &vectors {
            for &w in v.iter() {
                assert!(w >= -1e-10, "Weight should be >= 0, got {}", w);
            }
        }
    }

    #[test]
    fn test_weight_vector_3d() {
        let vectors = generate_uniform_weight_vectors(3, 20);
        assert!(!vectors.is_empty());

        for v in &vectors {
            assert_eq!(v.len(), 3);
            let sum: f64 = v.sum();
            assert!((sum - 1.0).abs() < 1e-10);
        }
    }

    #[test]
    fn test_neighborhood_computation() {
        let vectors = vec![array![1.0, 0.0], array![0.5, 0.5], array![0.0, 1.0]];

        let neighborhoods = compute_neighborhoods(&vectors, 2);
        assert_eq!(neighborhoods.len(), 3);
        // Each neighborhood should have 2 members
        for n in &neighborhoods {
            assert_eq!(n.len(), 2);
        }
    }

    #[test]
    fn test_scalarization_tchebycheff() {
        let config = MultiObjectiveConfig {
            population_size: 10,
            ..Default::default()
        };
        let moead = MOEAD::with_config(config, 2, 2, ScalarizationMethod::Tchebycheff);

        let objectives = array![2.0, 3.0];
        let weight = array![0.5, 0.5];
        let val = moead.scalar_value(&objectives, &weight);
        // With ideal point at infinity, this should be max(0.5 * |2 - inf|, 0.5 * |3 - inf|)
        // But ideal point is INF initially, so result is infinite
        // After initialization it would be sensible
        assert!(val.is_finite() || val.is_infinite()); // sanity check
    }

    #[test]
    fn test_moead_name() {
        let moead = MOEAD::new(50, 2, 3);
        assert_eq!(moead.name(), "MOEA/D");
    }

    #[test]
    fn test_binomial_coefficient() {
        assert_eq!(binomial_coefficient(5, 2), 10);
        assert_eq!(binomial_coefficient(10, 3), 120);
        assert_eq!(binomial_coefficient(4, 0), 1);
        assert_eq!(binomial_coefficient(0, 0), 1);
    }

    #[test]
    fn test_moead_weight_vectors_span_front() {
        // Verify that weight vectors cover the objective space with enough generations
        let config = MultiObjectiveConfig {
            max_generations: 30,
            population_size: 30,
            bounds: Some((Array1::zeros(3), Array1::ones(3))),
            random_seed: Some(77),
            neighborhood_size: Some(10),
            ..Default::default()
        };

        let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::Tchebycheff);
        let result = moead.optimize(zdt1).expect("should succeed");

        assert!(result.success);
        assert!(!result.pareto_front.is_empty());

        // The full solution set should have diversity
        let all_f1: Vec<f64> = result.population.iter().map(|s| s.objectives[0]).collect();
        let unique: std::collections::HashSet<u64> = all_f1.iter().map(|v| v.to_bits()).collect();
        assert!(unique.len() > 1, "Solutions should have varied f1 values");
    }

    #[test]
    fn test_moead_tchebycheff_vs_weighted_sum() {
        // Both methods should produce valid Pareto fronts
        let make_config = || MultiObjectiveConfig {
            max_generations: 15,
            population_size: 20,
            bounds: Some((Array1::zeros(3), Array1::ones(3))),
            random_seed: Some(42),
            neighborhood_size: Some(5),
            ..Default::default()
        };

        let mut moead_tch =
            MOEAD::with_config(make_config(), 2, 3, ScalarizationMethod::Tchebycheff);
        let result_tch = moead_tch
            .optimize(zdt1)
            .expect("Tchebycheff should succeed");

        let mut moead_ws =
            MOEAD::with_config(make_config(), 2, 3, ScalarizationMethod::WeightedSum);
        let result_ws = moead_ws.optimize(zdt1).expect("WeightedSum should succeed");

        // Both should produce non-empty Pareto fronts
        assert!(!result_tch.pareto_front.is_empty());
        assert!(!result_ws.pareto_front.is_empty());

        // Both should succeed
        assert!(result_tch.success);
        assert!(result_ws.success);
    }

    #[test]
    fn test_moead_ideal_point_tracking() {
        let config = MultiObjectiveConfig {
            max_generations: 10,
            population_size: 20,
            bounds: Some((Array1::zeros(3), Array1::ones(3))),
            random_seed: Some(42),
            neighborhood_size: Some(5),
            ..Default::default()
        };

        let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::Tchebycheff);
        let _result = moead.optimize(zdt1).expect("should succeed");

        // After optimization, ideal point should be finite (updated from initial infinity)
        assert!(
            moead.ideal_point[0].is_finite(),
            "Ideal point[0] should be finite after optimization"
        );
        assert!(
            moead.ideal_point[1].is_finite(),
            "Ideal point[1] should be finite after optimization"
        );
    }

    #[test]
    fn test_moead_deterministic_evaluation_count() {
        // With fixed parameters, evaluation count should be deterministic
        let config = MultiObjectiveConfig {
            max_generations: 5,
            population_size: 15,
            bounds: Some((Array1::zeros(3), Array1::ones(3))),
            random_seed: Some(42),
            neighborhood_size: Some(5),
            ..Default::default()
        };

        let mut moead = MOEAD::with_config(config, 2, 3, ScalarizationMethod::Tchebycheff);
        let result = moead.optimize(zdt1).expect("should succeed");

        // Should have evaluations > initial population
        assert!(result.n_evaluations > 10, "Should have > 10 evaluations");
        assert!(result.success);
        assert!(!result.pareto_front.is_empty());
    }

    #[test]
    fn test_moead_scalarization_values() {
        // Test that scalar_value produces expected results for known inputs
        let config = MultiObjectiveConfig {
            population_size: 10,
            ..Default::default()
        };
        let mut moead = MOEAD::with_config(config, 2, 2, ScalarizationMethod::WeightedSum);

        // Set ideal point to known values
        moead.ideal_point = array![0.0, 0.0];

        let objectives = array![2.0, 3.0];
        let weight = array![0.5, 0.5];

        // Weighted sum: 0.5*2 + 0.5*3 = 2.5
        let val = moead.scalar_value(&objectives, &weight);
        assert!(
            (val - 2.5).abs() < 1e-10,
            "WeightedSum should be 2.5, got {}",
            val
        );
    }

    #[test]
    fn test_moead_tchebycheff_scalarization_value() {
        let config = MultiObjectiveConfig {
            population_size: 10,
            ..Default::default()
        };
        let mut moead = MOEAD::with_config(config, 2, 2, ScalarizationMethod::Tchebycheff);
        moead.ideal_point = array![0.0, 0.0];

        let objectives = array![2.0, 3.0];
        let weight = array![0.5, 0.5];

        // Tchebycheff: max(0.5*|2-0|, 0.5*|3-0|) = max(1.0, 1.5) = 1.5
        let val = moead.scalar_value(&objectives, &weight);
        assert!(
            (val - 1.5).abs() < 1e-10,
            "Tchebycheff should be 1.5, got {}",
            val
        );
    }
}
