#[test]
#[ignore]
fn coo_no_duplicates_generates_admissible_matrices() {
    //TODO
}

#[cfg(feature = "slow-tests")]
mod slow {
    use nalgebra::DMatrix;
    use nalgebra_sparse::proptest::{
        coo_no_duplicates, coo_with_duplicates, csc, csr, sparsity_pattern,
    };

    use itertools::Itertools;
    use proptest::strategy::ValueTree;
    use proptest::test_runner::TestRunner;

    use proptest::prelude::*;

    use nalgebra_sparse::csr::CsrMatrix;
    use std::collections::HashSet;
    use std::iter::repeat;
    use std::ops::RangeInclusive;

    fn generate_all_possible_matrices(
        value_range: RangeInclusive<i32>,
        rows_range: RangeInclusive<usize>,
        cols_range: RangeInclusive<usize>,
    ) -> HashSet<DMatrix<i32>> {
        // Enumerate all possible combinations
        let mut all_combinations = HashSet::new();
        for nrows in rows_range {
            for ncols in cols_range.clone() {
                // For the given number of rows and columns
                let n_values = nrows * ncols;

                if n_values == 0 {
                    // If we have zero rows or columns, the set of matrices with the given
                    // rows and columns is a single element: an empty matrix
                    all_combinations.insert(DMatrix::from_row_slice(nrows, ncols, &[]));
                } else {
                    // Otherwise, we need to sample all possible matrices.
                    // To do this, we generate the values as the (multi) Cartesian product
                    // of the value sets. For example, for a 2x2 matrices, we consider
                    // all possible 4-element arrays that the matrices can take by
                    // considering all elements in the cartesian product
                    //  V x V x V x V
                    // where V is the set of eligible values, e.g. V := -1 ..= 1
                    let values_iter = repeat(value_range.clone())
                        .take(n_values)
                        .multi_cartesian_product();
                    for matrix_values in values_iter {
                        all_combinations.insert(DMatrix::from_row_slice(
                            nrows,
                            ncols,
                            &matrix_values,
                        ));
                    }
                }
            }
        }
        all_combinations
    }

    #[cfg(feature = "slow-tests")]
    #[test]
    fn coo_no_duplicates_samples_all_admissible_outputs() {
        // Note: This test basically mirrors a similar test for `matrix` in the `nalgebra` repo.

        // Test that the proptest generation covers all possible outputs for a small space of inputs
        // given enough samples.

        // We use a deterministic test runner to make the test "stable".
        let mut runner = TestRunner::deterministic();

        // This number needs to be high enough so that we with high probability sample
        // all possible cases
        let num_generated_matrices = 500000;

        let values = -1..=1;
        let rows = 0..=2;
        let cols = 0..=3;
        let max_nnz = rows.end() * cols.end();
        let strategy = coo_no_duplicates(values.clone(), rows.clone(), cols.clone(), max_nnz);

        // Enumerate all possible combinations
        let all_combinations = generate_all_possible_matrices(values, rows, cols);

        let visited_combinations =
            sample_matrix_output_space(strategy, &mut runner, num_generated_matrices);

        assert_eq!(visited_combinations.len(), all_combinations.len());
        assert_eq!(
            visited_combinations, all_combinations,
            "Did not sample all possible values."
        );
    }

    #[cfg(feature = "slow-tests")]
    #[test]
    fn coo_with_duplicates_samples_all_admissible_outputs() {
        // This is almost the same as the test for coo_no_duplicates, except that we need
        // a different "success" criterion, since coo_with_duplicates is able to generate
        // matrices with values outside of the value constraints. See below for details.

        // We use a deterministic test runner to make the test "stable".
        let mut runner = TestRunner::deterministic();

        // This number needs to be high enough so that we with high probability sample
        // all possible cases
        let num_generated_matrices = 750000;

        let values = -1..=1;
        let rows = 0..=2;
        let cols = 0..=3;
        let max_nnz = rows.end() * cols.end();
        let strategy = coo_with_duplicates(values.clone(), rows.clone(), cols.clone(), max_nnz, 2);

        // Enumerate all possible combinations that fit the constraints
        // (note: this is only a subset of the matrices that can be generated by
        // `coo_with_duplicates`)
        let all_combinations = generate_all_possible_matrices(values, rows, cols);

        let visited_combinations =
            sample_matrix_output_space(strategy, &mut runner, num_generated_matrices);

        // Here we cannot verify that the set of visited combinations is *equal* to
        // all possible outcomes with the given constraints, however the
        // strategy should be able to generate all matrices that fit the constraints.
        // In other words, we need to determine that set of all admissible matrices
        // is contained in the set of visited matrices
        assert!(all_combinations.is_subset(&visited_combinations));
    }

    #[cfg(feature = "slow-tests")]
    #[test]
    fn csr_samples_all_admissible_outputs() {
        // We use a deterministic test runner to make the test "stable".
        let mut runner = TestRunner::deterministic();

        // This number needs to be high enough so that we with high probability sample
        // all possible cases
        let num_generated_matrices = 500000;

        let values = -1..=1;
        let rows = 0..=2;
        let cols = 0..=3;
        let max_nnz = rows.end() * cols.end();
        let strategy = csr(values.clone(), rows.clone(), cols.clone(), max_nnz);

        let all_combinations = generate_all_possible_matrices(values, rows, cols);

        let visited_combinations =
            sample_matrix_output_space(strategy, &mut runner, num_generated_matrices);

        assert_eq!(visited_combinations.len(), all_combinations.len());
        assert_eq!(
            visited_combinations, all_combinations,
            "Did not sample all possible values."
        );
    }

    #[cfg(feature = "slow-tests")]
    #[test]
    fn csc_samples_all_admissible_outputs() {
        // We use a deterministic test runner to make the test "stable".
        let mut runner = TestRunner::deterministic();

        // This number needs to be high enough so that we with high probability sample
        // all possible cases
        let num_generated_matrices = 500000;

        let values = -1..=1;
        let rows = 0..=2;
        let cols = 0..=3;
        let max_nnz = rows.end() * cols.end();
        let strategy = csc(values.clone(), rows.clone(), cols.clone(), max_nnz);

        let all_combinations = generate_all_possible_matrices(values, rows, cols);

        let visited_combinations =
            sample_matrix_output_space(strategy, &mut runner, num_generated_matrices);

        assert_eq!(visited_combinations.len(), all_combinations.len());
        assert_eq!(
            visited_combinations, all_combinations,
            "Did not sample all possible values."
        );
    }

    #[cfg(feature = "slow-tests")]
    #[test]
    fn sparsity_pattern_samples_all_admissible_outputs() {
        let mut runner = TestRunner::deterministic();

        let num_generated_patterns = 50000;

        let major_dims = 0..=2;
        let minor_dims = 0..=3;
        let max_nnz = major_dims.end() * minor_dims.end();
        let strategy = sparsity_pattern(major_dims.clone(), minor_dims.clone(), max_nnz);

        let visited_patterns: HashSet<_> = sample_strategy(strategy, &mut runner)
            .take(num_generated_patterns)
            .map(|pattern| {
                // We represent patterns as dense matrices with 1 if an entry is occupied,
                // 0 otherwise
                let values = vec![1; pattern.nnz()];
                CsrMatrix::try_from_pattern_and_values(pattern, values).unwrap()
            })
            .map(|csr| DMatrix::from(&csr))
            .collect();

        let all_possible_patterns = generate_all_possible_matrices(0..=1, major_dims, minor_dims);

        assert_eq!(visited_patterns.len(), all_possible_patterns.len());
        assert_eq!(visited_patterns, all_possible_patterns);
    }

    fn sample_matrix_output_space<S>(
        strategy: S,
        runner: &mut TestRunner,
        num_samples: usize,
    ) -> HashSet<DMatrix<i32>>
    where
        S: Strategy,
        DMatrix<i32>: for<'b> From<&'b S::Value>,
    {
        sample_strategy(strategy, runner)
            .take(num_samples)
            .map(|matrix| DMatrix::from(&matrix))
            .collect()
    }

    fn sample_strategy<'a, S: 'a + Strategy>(
        strategy: S,
        runner: &'a mut TestRunner,
    ) -> impl 'a + Iterator<Item = S::Value> {
        repeat(()).map(move |_| {
            let tree = strategy
                .new_tree(runner)
                .expect("Tree generation should not fail");
            let value = tree.current();
            value
        })
    }
}
