pmat 3.14.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod quality_gate_threshold_tests {
    use super::*;

    // ===================
    // load_provability_threshold Tests (GH-172)
    // ===================

    #[test]
    fn test_load_provability_threshold_no_file() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let threshold = load_provability_threshold(temp_dir.path());
        assert!(
            (threshold - DEFAULT_PROVABILITY_THRESHOLD).abs() < f64::EPSILON,
            "Should fall back to default when file is missing"
        );
    }

    #[test]
    fn test_load_provability_threshold_from_config() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let config_content = r#"
[thresholds]
provability_min = 0.60
"#;
        std::fs::write(temp_dir.path().join(".pmat-metrics.toml"), config_content).unwrap();

        let threshold = load_provability_threshold(temp_dir.path());
        assert!(
            (threshold - 0.60).abs() < f64::EPSILON,
            "Should read provability_min from config, got {threshold}"
        );
    }

    #[test]
    fn test_load_provability_threshold_missing_key() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let config_content = r#"
[thresholds]
lint_max_ms = 150000
"#;
        std::fs::write(temp_dir.path().join(".pmat-metrics.toml"), config_content).unwrap();

        let threshold = load_provability_threshold(temp_dir.path());
        assert!(
            (threshold - DEFAULT_PROVABILITY_THRESHOLD).abs() < f64::EPSILON,
            "Should fall back to default when key is missing"
        );
    }

    #[test]
    fn test_load_provability_threshold_invalid_toml() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        std::fs::write(
            temp_dir.path().join(".pmat-metrics.toml"),
            "this is not valid toml {{{{",
        )
        .unwrap();

        let threshold = load_provability_threshold(temp_dir.path());
        assert!(
            (threshold - DEFAULT_PROVABILITY_THRESHOLD).abs() < f64::EPSILON,
            "Should fall back to default when TOML is invalid"
        );
    }

    #[test]
    fn test_load_provability_threshold_no_thresholds_section() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let config_content = r#"
[quality_gates]
min_coverage_pct = 85.0
"#;
        std::fs::write(temp_dir.path().join(".pmat-metrics.toml"), config_content).unwrap();

        let threshold = load_provability_threshold(temp_dir.path());
        assert!(
            (threshold - DEFAULT_PROVABILITY_THRESHOLD).abs() < f64::EPSILON,
            "Should fall back to default when [thresholds] section is missing"
        );
    }

    // --- Entropy threshold tests (#194) ---

    #[test]
    fn test_load_entropy_threshold_no_file() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let threshold = load_entropy_threshold(temp_dir.path(), 0.3);
        // #248: Empty temp dir has 0 source files (<10), so threshold is scaled by 0.5
        assert!(
            (threshold - 0.15).abs() < f64::EPSILON,
            "Should fall back to CLI value (0.3) scaled for small repo (0.5) = 0.15, got {threshold}"
        );
    }

    #[test]
    fn test_load_entropy_threshold_from_config() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let config_content = r#"
[thresholds]
entropy_min_diversity = 0.0
"#;
        std::fs::write(temp_dir.path().join(".pmat-metrics.toml"), config_content).unwrap();

        let threshold = load_entropy_threshold(temp_dir.path(), 0.3);
        assert!(
            threshold.abs() < f64::EPSILON,
            "Should read entropy_min_diversity=0.0 from config, got {threshold}"
        );
    }

    #[test]
    fn test_load_entropy_threshold_missing_key_falls_back_to_cli() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let config_content = r#"
[thresholds]
provability_min = 0.70
"#;
        std::fs::write(temp_dir.path().join(".pmat-metrics.toml"), config_content).unwrap();

        let threshold = load_entropy_threshold(temp_dir.path(), 0.5);
        // #248: Empty temp dir has 0 source files (<10), so threshold is scaled by 0.5
        assert!(
            (threshold - 0.25).abs() < f64::EPSILON,
            "Should fall back to CLI value (0.5) scaled for small repo (0.5) = 0.25, got {threshold}"
        );
    }

    // --- Exclude paths tests (#195) ---

    #[test]
    fn test_load_entropy_exclude_paths_no_file() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let paths = load_entropy_exclude_paths(temp_dir.path());
        assert!(paths.is_empty(), "Should return empty when file is missing");
    }

    #[test]
    fn test_load_entropy_exclude_paths_from_exclude_section() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let config_content = r#"
[exclude]
paths = ["reference/", "vendor/"]
"#;
        std::fs::write(temp_dir.path().join(".pmat-metrics.toml"), config_content).unwrap();

        let paths = load_entropy_exclude_paths(temp_dir.path());
        assert_eq!(paths, vec!["reference/", "vendor/"]);
    }

    #[test]
    fn test_load_entropy_exclude_paths_from_top_level() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let config_content = r#"
exclude_paths = ["third_party/"]
"#;
        std::fs::write(temp_dir.path().join(".pmat-metrics.toml"), config_content).unwrap();

        let paths = load_entropy_exclude_paths(temp_dir.path());
        assert_eq!(paths, vec!["third_party/"]);
    }

    #[test]
    fn test_load_entropy_exclude_paths_section_takes_precedence() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let config_content = r#"
exclude_paths = ["old/"]

[exclude]
paths = ["new/"]
"#;
        std::fs::write(temp_dir.path().join(".pmat-metrics.toml"), config_content).unwrap();

        let paths = load_entropy_exclude_paths(temp_dir.path());
        assert_eq!(paths, vec!["new/"], "[exclude] paths should take precedence over top-level exclude_paths");
    }

    #[test]
    fn test_load_entropy_exclude_paths_from_gates_toml() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let gates_content = r#"
[quality-gates]
exclude = [
    "**/*_generated.rs",
    "demos/**",
    "examples/**",
]
"#;
        std::fs::write(temp_dir.path().join(".pmat-gates.toml"), gates_content).unwrap();

        let paths = load_entropy_exclude_paths(temp_dir.path());
        assert_eq!(paths.len(), 3, "Should load excludes from .pmat-gates.toml [quality-gates] exclude");
        assert!(paths.contains(&"**/*_generated.rs".to_string()));
        assert!(paths.contains(&"demos/**".to_string()));
        assert!(paths.contains(&"examples/**".to_string()));
    }

    #[test]
    fn test_load_entropy_exclude_paths_merges_both_files() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let metrics_content = r#"
[exclude]
paths = ["target/**"]
"#;
        let gates_content = r#"
[quality-gates]
exclude = ["demos/**", "examples/**"]
"#;
        std::fs::write(temp_dir.path().join(".pmat-metrics.toml"), metrics_content).unwrap();
        std::fs::write(temp_dir.path().join(".pmat-gates.toml"), gates_content).unwrap();

        let paths = load_entropy_exclude_paths(temp_dir.path());
        assert_eq!(paths.len(), 3, "Should merge excludes from both config files");
        assert!(paths.contains(&"target/**".to_string()));
        assert!(paths.contains(&"demos/**".to_string()));
        assert!(paths.contains(&"examples/**".to_string()));
    }

    // --- Entropy gate config tests (#220) ---

    #[test]
    fn test_load_entropy_gate_config_no_file() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let config = load_entropy_gate_config(temp_dir.path());
        assert!(config.enabled);
        assert!(config.max_violations.is_none());
        assert!(config.exclude.is_empty());
    }

    #[test]
    fn test_load_entropy_gate_config_enabled_false() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let content = r#"
[entropy]
enabled = false
"#;
        std::fs::write(temp_dir.path().join(".pmat-gates.toml"), content).unwrap();
        let config = load_entropy_gate_config(temp_dir.path());
        assert!(!config.enabled);
    }

    #[test]
    fn test_load_entropy_gate_config_max_violations() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let content = r#"
[entropy]
max_violations = 5
"#;
        std::fs::write(temp_dir.path().join(".pmat-gates.toml"), content).unwrap();
        let config = load_entropy_gate_config(temp_dir.path());
        assert!(config.enabled);
        assert_eq!(config.max_violations, Some(5));
    }

    #[test]
    fn test_load_entropy_gate_config_excludes() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let content = r#"
[entropy]
exclude = ["**/gqa.rs", "benches/**"]
"#;
        std::fs::write(temp_dir.path().join(".pmat-gates.toml"), content).unwrap();
        let config = load_entropy_gate_config(temp_dir.path());
        assert_eq!(config.exclude.len(), 2);
        assert!(config.exclude.contains(&"**/gqa.rs".to_string()));
        assert!(config.exclude.contains(&"benches/**".to_string()));
    }

    #[test]
    fn test_load_entropy_gate_config_full() {
        let temp_dir = tempfile::TempDir::new().unwrap();
        let content = r#"
[entropy]
enabled = true
max_pattern_repetition = 12
min_pattern_diversity = 0.3
max_violations = 3
exclude = ["**/gqa.rs"]
"#;
        std::fs::write(temp_dir.path().join(".pmat-gates.toml"), content).unwrap();
        let config = load_entropy_gate_config(temp_dir.path());
        assert!(config.enabled);
        assert_eq!(config.max_violations, Some(3));
        assert_eq!(config.exclude, vec!["**/gqa.rs"]);
    }

    // --- Filter violations by exclude paths tests (#196) ---

    #[test]
    fn test_filter_violations_excludes_matching_files() {
        let mut violations = vec![
            QualityViolation { check_type: "satd".into(), severity: "warning".into(), file: "reference/kong/init.lua".into(), line: Some(10), message: "TODO".into(), details: None },
            QualityViolation { check_type: "satd".into(), severity: "warning".into(), file: "src/main.rs".into(), line: Some(5), message: "TODO".into(), details: None },
            QualityViolation { check_type: "duplicates".into(), severity: "info".into(), file: "reference/apisix/core.lua".into(), line: None, message: "dup".into(), details: None },
        ];
        let excludes = vec!["reference/".to_string()];
        filter_violations_by_exclude(&mut violations, &excludes);
        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].file, "src/main.rs");
    }

    #[test]
    fn test_filter_violations_keeps_project_level() {
        let mut violations = vec![
            QualityViolation { check_type: "entropy".into(), severity: "warning".into(), file: "project".into(), line: None, message: "low diversity".into(), details: None },
            QualityViolation { check_type: "satd".into(), severity: "info".into(), file: "vendor/lib.lua".into(), line: Some(1), message: "hack".into(), details: None },
        ];
        let excludes = vec!["vendor/".to_string()];
        filter_violations_by_exclude(&mut violations, &excludes);
        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].file, "project");
    }

    #[test]
    fn test_filter_violations_empty_excludes_no_change() {
        let mut violations = vec![
            QualityViolation { check_type: "satd".into(), severity: "info".into(), file: "src/lib.rs".into(), line: Some(1), message: "fixme".into(), details: None },
        ];
        filter_violations_by_exclude(&mut violations, &[]);
        assert_eq!(violations.len(), 1);
    }

    #[test]
    fn test_recalculate_from_violations() {
        let violations = vec![
            QualityViolation { check_type: "satd".into(), severity: "warning".into(), file: "a.rs".into(), line: Some(1), message: "todo".into(), details: None },
            QualityViolation { check_type: "satd".into(), severity: "info".into(), file: "b.rs".into(), line: Some(2), message: "hack".into(), details: None },
            QualityViolation { check_type: "complexity".into(), severity: "error".into(), file: "c.rs".into(), line: Some(3), message: "high".into(), details: None },
        ];
        let mut results = QualityGateResults::default();
        results.recalculate_from(&violations);
        assert_eq!(results.satd_violations, 2);
        assert_eq!(results.complexity_violations, 1);
        assert_eq!(results.total_violations, 3);
        assert_eq!(results.entropy_violations, 0);
    }
}