//! Simplified property-based tests for clustering algorithms
//!
//! This module contains property-based tests that verify fundamental clustering properties
//! for algorithms that have working APIs.

use proptest::prelude::*;
use proptest::strategy::ValueTree;
use scirs2_core::ndarray::{Array1, Array2};
use sklears_clustering::{ClusteringValidator, KMeans, KMeansConfig};
use sklears_core::{
    traits::{Fit, Predict},
    types::Float,
};

/// Generate random data for testing
fn random_data_strategy() -> impl Strategy<Value = Array2<Float>> {
    (2usize..=5, 2usize..=4, 20usize..=100).prop_flat_map(|(n_features, _n_clusters, n_samples)| {
        prop::collection::vec(prop::collection::vec(-10.0f64..10.0, n_features), n_samples)
            .prop_map(move |data| {
                Array2::from_shape_vec(
                    (n_samples, n_features),
                    data.into_iter().flatten().collect(),
                )
                .expect("operation should succeed")
            })
    })
}

/// Test that K-Means assigns every point to some cluster
mod kmeans_completeness {
    use super::*;

    proptest! {
        #[test]
        fn kmeans_assigns_all_points(data in random_data_strategy()) {
            let (n_samples, n_features) = data.dim();
            if n_samples < 4 || n_features < 1 {
                return Ok(());
            }

            let n_clusters = (n_samples / 4).clamp(2, 6);
            let config = KMeansConfig {
                n_clusters,
                random_seed: Some(42),
                ..Default::default()
            };
            let kmeans = KMeans::new(config);
            let y_dummy = Array1::zeros(n_samples);

            if let Ok(fitted) = kmeans.fit(&data, &y_dummy) {
                if let Ok(labels) = fitted.predict(&data) {
                    // Every point should be assigned to a cluster
                    prop_assert_eq!(labels.len(), n_samples);
                    // All labels should be valid cluster indices
                    for &label in labels.iter() {
                        prop_assert!(label < n_clusters as i32);
                    }

                    // Should find exactly n_clusters unique labels
                    let unique_labels: std::collections::HashSet<_> = labels.iter().collect();
                    prop_assert_eq!(unique_labels.len(), n_clusters);
                }
            }
        }
    }
}

/// Test clustering stability with same seed
mod kmeans_stability {
    use super::*;

    proptest! {
        #[test]
        #[ignore] // Disabled due to inherent instability with sparse/edge-case data
        fn kmeans_stability_with_same_seed(data in random_data_strategy()) {
            let (n_samples, n_features) = data.dim();
            if n_samples < 4 || n_features < 1 {
                return Ok(());
            }

            let n_clusters = (n_samples / 4).clamp(2, 4);

            let config1 = KMeansConfig {
                n_clusters,
                random_seed: Some(12345),
                ..Default::default()
            };
            let config2 = KMeansConfig {
                n_clusters,
                random_seed: Some(12345),
                ..Default::default()
            };
            let kmeans1 = KMeans::new(config1);
            let kmeans2 = KMeans::new(config2);
            let y_dummy = Array1::zeros(n_samples);

            if let (Ok(fitted1), Ok(fitted2)) = (kmeans1.fit(&data, &y_dummy), kmeans2.fit(&data, &y_dummy)) {
                if let (Ok(labels1), Ok(labels2)) = (fitted1.predict(&data), fitted2.predict(&data)) {
                    // With same seed, results should be identical
                    prop_assert_eq!(labels1, labels2);
                }
            }
        }
    }
}

/// Test clustering invariance under translation
mod kmeans_invariance {
    use super::*;

    proptest! {
        #[test]
        #[ignore] // Disabled due to inherent instability with sparse/edge-case data
        fn kmeans_translation_invariance(
            data in random_data_strategy(),
            translation in prop::collection::vec(-5.0f64..5.0, 1..=5)
        ) {
            let (n_samples, n_features) = data.dim();
            if n_samples < 4 || n_features < 1 || translation.len() != n_features {
                return Ok(());
            }

            // Create translated data
            let mut translated_data = data.clone();
            for i in 0..n_samples {
                for j in 0..n_features {
                    translated_data[[i, j]] += translation[j];
                }
            }

            let n_clusters = (n_samples / 4).clamp(2, 4);

            let config1 = KMeansConfig {
                n_clusters,
                random_seed: Some(42),
                ..Default::default()
            };
            let config2 = KMeansConfig {
                n_clusters,
                random_seed: Some(42),
                ..Default::default()
            };
            let kmeans1 = KMeans::new(config1);
            let kmeans2 = KMeans::new(config2);
            let y_dummy = Array1::zeros(n_samples);

            if let (Ok(fitted1), Ok(fitted2)) = (kmeans1.fit(&data, &y_dummy), kmeans2.fit(&translated_data, &y_dummy)) {
                if let (Ok(labels1), Ok(labels2)) = (fitted1.predict(&data), fitted2.predict(&translated_data)) {
                    // Translation should not change cluster assignments
                    prop_assert_eq!(labels1, labels2);
                }
            }
        }
    }
}

/// Test clustering consistency
mod kmeans_consistency {
    use super::*;

    proptest! {
        #[test]
        fn kmeans_cluster_count_consistency(data in random_data_strategy()) {
            let (n_samples, n_features) = data.dim();
            if n_samples < 4 || n_features < 1 {
                return Ok(());
            }

            let n_clusters = (n_samples / 4).clamp(2, 6);
            let config = KMeansConfig {
                n_clusters,
                random_seed: Some(42),
                ..Default::default()
            };
            let kmeans = KMeans::new(config);
            let y_dummy = Array1::zeros(n_samples);

            if let Ok(fitted) = kmeans.fit(&data, &y_dummy) {
                if let Ok(labels) = fitted.predict(&data) {
                    let unique_labels: std::collections::HashSet<_> = labels.iter().collect();
                    // Should have exactly n_clusters unique labels
                    prop_assert_eq!(unique_labels.len(), n_clusters);

                    // All cluster IDs should be in range [0, n_clusters)
                    for &label in labels.iter() {
                        prop_assert!(label < n_clusters as i32);
                    }
                }
            }
        }

        #[test]
        fn kmeans_inertia_properties(data in random_data_strategy()) {
            let (n_samples, n_features) = data.dim();
            if n_samples < 4 || n_features < 1 {
                return Ok(());
            }

            let n_clusters = (n_samples / 4).clamp(2, 4);
            let config = KMeansConfig {
                n_clusters,
                random_seed: Some(42),
                ..Default::default()
            };
            let kmeans = KMeans::new(config);
            let y_dummy = Array1::zeros(n_samples);

            if let Ok(_fitted) = kmeans.fit(&data, &y_dummy) {
                // Inertia method not available in current API
                // TODO: Re-enable when inertia() is exposed
            }
        }
    }
}

/// Test validation metrics properties
mod validation_properties {
    use super::*;

    proptest! {
        #[test]
        fn silhouette_score_bounds(data in random_data_strategy()) {
            let (n_samples, n_features) = data.dim();
            if n_samples < 6 || n_features < 1 {
                return Ok(());
            }

            let n_clusters = (n_samples / 5).clamp(2, 4);
            let config = KMeansConfig {
                n_clusters,
                random_seed: Some(42),
                ..Default::default()
            };
            let kmeans = KMeans::new(config);
            let y_dummy = Array1::zeros(n_samples);

            if let Ok(fitted) = kmeans.fit(&data, &y_dummy) {
                if let Ok(labels) = fitted.predict(&data) {
                    let validator = ClusteringValidator::euclidean();
                    let labels_i32: Vec<i32> = labels.to_vec();

                    if let Ok(silhouette_result) = validator.silhouette_analysis(&data, &labels_i32) {
                        // Silhouette score should be between -1 and 1
                        prop_assert!(silhouette_result.mean_silhouette >= -1.0);
                        prop_assert!(silhouette_result.mean_silhouette <= 1.0);

                        // Individual scores should also be in bounds
                        for &score in silhouette_result.sample_silhouettes.iter() {
                            prop_assert!(score >= -1.0);
                            prop_assert!(score <= 1.0);
                        }
                    }
                }
            }
        }

        #[test]
        fn davies_bouldin_index_properties(data in random_data_strategy()) {
            let (n_samples, n_features) = data.dim();
            if n_samples < 6 || n_features < 1 {
                return Ok(());
            }

            let n_clusters = (n_samples / 5).clamp(2, 4);
            let config = KMeansConfig {
                n_clusters,
                random_seed: Some(42),
                ..Default::default()
            };
            let kmeans = KMeans::new(config);
            let y_dummy = Array1::zeros(n_samples);

            if let Ok(fitted) = kmeans.fit(&data, &y_dummy) {
                if let Ok(labels) = fitted.predict(&data) {
                    let validator = ClusteringValidator::euclidean();
                    let labels_i32: Vec<i32> = labels.to_vec();

                    if let Ok(db_index) = validator.davies_bouldin_index(&data, &labels_i32) {
                        // Davies-Bouldin index should be non-negative
                        prop_assert!(db_index >= 0.0, "DB index should be non-negative: {}", db_index);
                    }
                }
            }
        }
    }
}

/// Test edge cases and robustness
mod robustness_tests {
    use super::*;

    proptest! {
        #[test]
        fn handle_identical_points(n_points in 5usize..20, n_features in 2usize..5) {
            // All points are identical
            let data = Array2::from_elem((n_points, n_features), 1.0);

            let n_clusters = (n_points / 3).clamp(2, 5);
            let config = KMeansConfig {
                n_clusters,
                random_seed: Some(42),
                ..Default::default()
            };
            let kmeans = KMeans::new(config);
            let y_dummy = Array1::zeros(n_points);

            // Should handle identical points gracefully
            if let Ok(fitted) = kmeans.fit(&data, &y_dummy) {
                if let Ok(labels) = fitted.predict(&data) {
                    // All points might be assigned to one cluster
                    let unique_labels: std::collections::HashSet<_> = labels.iter().collect();
                    prop_assert!(!unique_labels.is_empty());
                    prop_assert!(unique_labels.len() <= n_clusters);

                    // All labels should be valid
                    for &label in labels.iter() {
                        prop_assert!(label < n_clusters as i32);
                    }
                }
            }
        }

        #[test]
        fn handle_single_point_clusters(n_clusters in 2usize..6) {
            // Create data where each point could be its own cluster
            let mut data_vec = Vec::new();
            for i in 0..n_clusters {
                data_vec.extend(vec![(i as f64) * 10.0, (i as f64) * 10.0]);
            }

            let data = Array2::from_shape_vec((n_clusters, 2), data_vec).expect("operation should succeed");

            let config = KMeansConfig {
                n_clusters,
                random_seed: Some(42),
                ..Default::default()
            };
            let kmeans = KMeans::new(config);
            let y_dummy = Array1::zeros(n_clusters);

            if let Ok(fitted) = kmeans.fit(&data, &y_dummy) {
                if let Ok(labels) = fitted.predict(&data) {
                    // Should assign each point to a different cluster
                    let unique_labels: std::collections::HashSet<_> = labels.iter().collect();
                    prop_assert_eq!(unique_labels.len(), n_clusters);

                    // All labels should be valid
                    for &label in labels.iter() {
                        prop_assert!(label < n_clusters as i32);
                    }
                }
            }
        }
    }
}

#[allow(non_snake_case)]
#[cfg(test)]
mod test_utils {
    use super::*;

    #[test]
    fn test_data_generation() {
        // Test that our data generation strategy works
        let strategy = random_data_strategy();
        let mut runner = proptest::test_runner::TestRunner::default();
        let data = strategy
            .new_tree(&mut runner)
            .expect("operation should succeed")
            .current();

        assert!(data.nrows() >= 20);
        assert!(data.ncols() >= 2);
        assert!(data.ncols() <= 5);
    }
}
