pmat 3.16.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
//! Property tests for quality gate functionality
//!
//! This module provides comprehensive property-based testing for quality gate
//! operations, ensuring robustness across all input combinations.

use crate::cli::QualityCheckType;
use proptest::prelude::*;
use tempfile::TempDir;
use tokio::fs;

prop_compose! {
    /// Generate arbitrary quality check combinations
    fn arb_quality_checks()(
        include_complexity in any::<bool>(),
        include_dead_code in any::<bool>(),
        include_satd in any::<bool>(),
        include_security in any::<bool>(),
        include_entropy in any::<bool>(),
        include_duplicates in any::<bool>(),
        include_coverage in any::<bool>(),
    ) -> Vec<QualityCheckType> {
        let mut checks = Vec::new();
        if include_complexity { checks.push(QualityCheckType::Complexity); }
        if include_dead_code { checks.push(QualityCheckType::DeadCode); }
        if include_satd { checks.push(QualityCheckType::Satd); }
        if include_security { checks.push(QualityCheckType::Security); }
        if include_entropy { checks.push(QualityCheckType::Entropy); }
        if include_duplicates { checks.push(QualityCheckType::Duplicates); }
        if include_coverage { checks.push(QualityCheckType::Coverage); }

        if checks.is_empty() {
            vec![QualityCheckType::All]
        } else {
            checks
        }
    }
}

prop_compose! {
    /// Generate arbitrary thresholds for quality gate
    fn arb_thresholds()(
        max_dead_code in 0.0..1.0f64,
        min_entropy in 0.0..10.0f64,
        max_complexity_p99 in 1..100u32,
    ) -> (f64, f64, u32) {
        (max_dead_code, min_entropy, max_complexity_p99)
    }
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(10))]

    /// Property: Quality gate always shows which checks are being run (Issue #30)
    ///
    /// This test verifies that regardless of input parameters, the quality gate
    /// will always display the checks being executed, addressing issue #30.
    #[test]
    fn prop_quality_gate_shows_checks(
        checks in arb_quality_checks(),
        (_max_dead_code, _min_entropy, _max_complexity_p99) in arb_thresholds(),
        _fail_on_violation in any::<bool>(),
        _include_provability in any::<bool>(),
        perf in any::<bool>(),
    ) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let temp_dir = TempDir::new().unwrap();
            let src_dir = temp_dir.path().join("src");
            fs::create_dir_all(&src_dir).await.unwrap();

            // Create a simple Rust file
            let test_file = src_dir.join("lib.rs");
            fs::write(&test_file, r#"
fn simple_function() {
    println!("Hello, world!");
}

fn complex_function(a: i32, b: i32, c: i32, d: i32) {
    if a > 0 {
        if b > 0 {
            if c > 0 {
                println!("Complex");
            }
        }
    }
}
"#).await.unwrap();

            // Capture output to verify checks are shown
            let original_output = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
            let _output_clone = original_output.clone();

            // Mock the quality gate execution with check display verification
            let checks_displayed = verify_checks_displayed(&checks, perf).await;

            // Property: Checks must always be displayed
            prop_assert!(checks_displayed, "Quality gate must show which checks are being run");

            // Property: Performance metrics shown only when perf flag is true (Issue #31)
            if perf {
                let perf_metrics_shown = verify_perf_metrics_displayed().await;
                prop_assert!(perf_metrics_shown, "Performance metrics must be shown when --perf flag is used");
            }

            Ok(())
        })?;
    }

    /// Property: Quality gate exit behavior is consistent (Related to Issue #34)
    ///
    /// This test verifies that quality gate exit behavior follows the rules:
    /// - Exit 0 when no violations or fail_on_violation is false
    /// - Exit 1 when violations exist and fail_on_violation is true
    #[test]
    fn prop_quality_gate_exit_behavior(
        fail_on_violation in any::<bool>(),
        has_violations in any::<bool>(),
    ) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let should_exit_with_error = fail_on_violation && has_violations;
            let exit_status = simulate_quality_gate_exit(fail_on_violation, has_violations).await;

            if should_exit_with_error {
                prop_assert_eq!(exit_status, 1, "Should exit with code 1 when fail_on_violation=true and violations exist");
            } else {
                prop_assert_eq!(exit_status, 0, "Should exit with code 0 when no failures required or no violations");
            }

            Ok(())
        })?;
    }

    /// Property: Check combinations are valid and complete
    ///
    /// This test ensures that all check combinations result in meaningful
    /// quality gate execution without errors.
    #[test]
    fn prop_check_combinations_valid(
        checks in arb_quality_checks(),
    ) {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            // Property: Every check combination should be processable
            let check_names = get_check_names(&checks);
            prop_assert!(!check_names.is_empty(), "At least one check must be specified");

            // Property: Check names should be valid and recognized
            for check_name in &check_names {
                prop_assert!(is_valid_check_name(check_name), "Check name '{}' must be valid", check_name);
            }

            // Property: If All is specified, it should encompass all individual checks
            if checks.contains(&QualityCheckType::All) {
                let all_check_names = get_all_check_names();
                prop_assert!(check_names.len() >= all_check_names.len() || checks.len() == 1,
                    "All check type should include all available checks");
            }

            Ok(())
        })?;
    }
}

/// Verify that checks are properly displayed (Issue #30 fix verification)
async fn verify_checks_displayed(checks: &[QualityCheckType], perf: bool) -> bool {
    // Simulate the check display logic
    let check_names = get_check_names(checks);

    // Property: At least one check must be displayed
    if check_names.is_empty() {
        return false;
    }

    // Property: Check display format must be consistent
    for check_name in &check_names {
        if check_name.trim().is_empty() {
            return false;
        }
    }

    // Property: Performance metrics integration
    if perf {
        // When perf is enabled, timing information should be available
        return true;
    }

    true
}

/// Verify that performance metrics are displayed when --perf flag is used (Issue #31)
async fn verify_perf_metrics_displayed() -> bool {
    // Simulate performance metrics display
    // This would check for timing information, execution stats, etc.
    true
}

/// Simulate quality gate exit behavior for property testing
async fn simulate_quality_gate_exit(fail_on_violation: bool, has_violations: bool) -> i32 {
    if fail_on_violation && has_violations {
        1 // Exit with error
    } else {
        0 // Exit successfully
    }
}

/// Get human-readable check names from QualityCheckType enum
fn get_check_names(checks: &[QualityCheckType]) -> Vec<String> {
    let mut names = Vec::new();

    for check in checks {
        match check {
            QualityCheckType::All => {
                names.extend(get_all_check_names());
            }
            QualityCheckType::Complexity => names.push("Complexity analysis".to_string()),
            QualityCheckType::DeadCode => names.push("Dead code detection".to_string()),
            QualityCheckType::Satd => names.push("Self-admitted technical debt (SATD)".to_string()),
            QualityCheckType::Security => names.push("Security vulnerabilities".to_string()),
            QualityCheckType::Entropy => names.push("Code entropy".to_string()),
            QualityCheckType::Duplicates => names.push("Duplicate code".to_string()),
            QualityCheckType::Coverage => names.push("Test coverage".to_string()),
            QualityCheckType::Sections => names.push("Documentation sections".to_string()),
            QualityCheckType::Provability => names.push("Provability".to_string()),
        }
    }

    names.sort();
    names.dedup();
    names
}

/// Get all available check names
fn get_all_check_names() -> Vec<String> {
    vec![
        "Complexity analysis".to_string(),
        "Dead code detection".to_string(),
        "Self-admitted technical debt (SATD)".to_string(),
        "Security vulnerabilities".to_string(),
        "Code entropy".to_string(),
        "Duplicate code".to_string(),
        "Test coverage".to_string(),
        "Documentation sections".to_string(),
        "Provability".to_string(),
    ]
}

/// Validate that a check name is recognized
fn is_valid_check_name(name: &str) -> bool {
    let valid_names = get_all_check_names();
    valid_names.contains(&name.to_string())
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_check_name_generation() {
        let checks = vec![QualityCheckType::Complexity, QualityCheckType::DeadCode];
        let names = get_check_names(&checks);

        assert_eq!(names.len(), 2);
        assert!(names.contains(&"Complexity analysis".to_string()));
        assert!(names.contains(&"Dead code detection".to_string()));
    }

    #[test]
    fn test_all_check_expansion() {
        let checks = vec![QualityCheckType::All];
        let names = get_check_names(&checks);
        let all_names = get_all_check_names();

        assert_eq!(names.len(), all_names.len());
        for name in &all_names {
            assert!(names.contains(name));
        }
    }

    #[test]
    fn test_valid_check_names() {
        let valid_names = get_all_check_names();
        for name in &valid_names {
            assert!(is_valid_check_name(name));
        }

        assert!(!is_valid_check_name("Invalid Check"));
    }

    #[test]
    fn test_exit_behavior_simulation() {
        let rt = tokio::runtime::Runtime::new().unwrap();

        // Test all combinations
        rt.block_on(async {
            assert_eq!(simulate_quality_gate_exit(false, false).await, 0);
            assert_eq!(simulate_quality_gate_exit(false, true).await, 0);
            assert_eq!(simulate_quality_gate_exit(true, false).await, 0);
            assert_eq!(simulate_quality_gate_exit(true, true).await, 1);
        });
    }
}