pmat 3.11.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
#![cfg_attr(coverage_nightly, coverage(off))]

use minijinja::Value;

/// Register all custom helpers (filters + functions) on a minijinja Environment.
pub fn register_helpers(env: &mut minijinja::Environment<'_>) {
    env.add_filter("snake_case", snake_case_filter);
    env.add_filter("kebab_case", kebab_case_filter);
    env.add_filter("pascal_case", pascal_case_filter);
    env.add_function("current_year", current_year_fn);
    env.add_function("current_date", current_date_fn);
}

fn snake_case_filter(value: &str) -> String {
    to_snake_case(value)
}

fn kebab_case_filter(value: &str) -> String {
    to_kebab_case(value)
}

fn pascal_case_filter(value: &str) -> String {
    to_pascal_case(value)
}

fn current_year_fn() -> Value {
    Value::from(chrono::Utc::now().format("%Y").to_string())
}

fn current_date_fn() -> Value {
    Value::from(chrono::Utc::now().format("%Y-%m-%d").to_string())
}

// Case conversion utilities
pub fn to_snake_case(s: &str) -> String {
    let mut result = String::with_capacity(1024);
    let mut prev_is_upper = false;

    for (i, ch) in s.chars().enumerate() {
        if ch.is_uppercase() && i > 0 && !prev_is_upper {
            result.push('_');
        }
        // to_lowercase() always returns at least one character for any valid char
        result.push(
            ch.to_lowercase()
                .next()
                .expect("to_lowercase() always yields at least one char"),
        );
        prev_is_upper = ch.is_uppercase();
    }

    result.replace(['-', ' '], "_")
}

fn to_kebab_case(s: &str) -> String {
    to_snake_case(s).replace('_', "-")
}

pub fn to_pascal_case(s: &str) -> String {
    s.split(['_', '-', ' '])
        .filter(|s| !s.is_empty())
        .map(|s| {
            let mut chars = s.chars();
            match chars.next() {
                None => String::with_capacity(1024),
                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
            }
        })
        .collect()
}

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

    #[test]
    fn test_to_snake_case_basic() {
        assert_eq!(to_snake_case("MyProjectName"), "my_project_name");
        assert_eq!(to_snake_case("camelCase"), "camel_case");
        assert_eq!(to_snake_case("PascalCase"), "pascal_case");
        assert_eq!(to_snake_case("snake_case"), "snake_case");
        assert_eq!(to_snake_case("kebab-case"), "kebab_case");
        assert_eq!(to_snake_case("With Spaces"), "with__spaces"); // Double underscore for spaces
        assert_eq!(to_snake_case(""), "");
    }

    #[test]
    fn test_to_snake_case_edge_cases() {
        assert_eq!(to_snake_case("XMLHttpRequest"), "xmlhttp_request");
        assert_eq!(to_snake_case("IOError"), "ioerror");
        assert_eq!(to_snake_case("123Numbers"), "123_numbers");
        assert_eq!(to_snake_case("UPPERCASE"), "uppercase");
        assert_eq!(to_snake_case("lowercase"), "lowercase");
    }

    #[test]
    fn test_to_kebab_case_basic() {
        assert_eq!(to_kebab_case("MyProjectName"), "my-project-name");
        assert_eq!(to_kebab_case("snake_case"), "snake-case");
        assert_eq!(to_kebab_case("With Spaces"), "with--spaces"); // Double dash for spaces
    }

    #[test]
    fn test_to_pascal_case_basic() {
        assert_eq!(to_pascal_case("my_project_name"), "MyProjectName");
        assert_eq!(to_pascal_case("kebab-case-name"), "KebabCaseName");
        assert_eq!(to_pascal_case("with spaces here"), "WithSpacesHere");
        assert_eq!(to_pascal_case(""), "");
        assert_eq!(to_pascal_case("single"), "Single");
    }

    #[test]
    fn test_snake_case_filter_with_minijinja() {
        let mut env = minijinja::Environment::new();
        register_helpers(&mut env);

        let template = env.template_from_str("{{ name|snake_case }}").unwrap();
        let result = template
            .render(minijinja::context! { name => "MyProjectName" })
            .unwrap();
        assert_eq!(result, "my_project_name");
    }

    #[test]
    fn test_kebab_case_filter_with_minijinja() {
        let mut env = minijinja::Environment::new();
        register_helpers(&mut env);

        let template = env.template_from_str("{{ name|kebab_case }}").unwrap();
        let result = template
            .render(minijinja::context! { name => "MyProjectName" })
            .unwrap();
        assert_eq!(result, "my-project-name");
    }

    #[test]
    fn test_pascal_case_filter_with_minijinja() {
        let mut env = minijinja::Environment::new();
        register_helpers(&mut env);

        let template = env.template_from_str("{{ name|pascal_case }}").unwrap();
        let result = template
            .render(minijinja::context! { name => "my_project_name" })
            .unwrap();
        assert_eq!(result, "MyProjectName");
    }

    #[test]
    fn test_current_year_fn_with_minijinja() {
        let mut env = minijinja::Environment::new();
        register_helpers(&mut env);

        let template = env.template_from_str("{{ current_year() }}").unwrap();
        let result = template.render(minijinja::context! {}).unwrap();

        // Verify it's a valid year
        let year: u32 = result.parse().expect("Should be a valid year");
        assert!((2024..=2100).contains(&year));
    }

    #[test]
    fn test_current_date_fn_with_minijinja() {
        let mut env = minijinja::Environment::new();
        register_helpers(&mut env);

        let template = env.template_from_str("{{ current_date() }}").unwrap();
        let result = template.render(minijinja::context! {}).unwrap();

        // Check format YYYY-MM-DD
        assert_eq!(result.len(), 10);
        assert_eq!(result.chars().nth(4), Some('-'));
        assert_eq!(result.chars().nth(7), Some('-'));
    }

    #[test]
    fn test_to_pascal_case_empty_segment() {
        // Tests the None branch in to_pascal_case when splitting produces empty segments
        assert_eq!(to_pascal_case("__double__underscore__"), "DoubleUnderscore");
        assert_eq!(to_pascal_case("--double--dash--"), "DoubleDash");
        assert_eq!(to_pascal_case("  double  space  "), "DoubleSpace");
    }

    #[test]
    fn test_to_pascal_case_single_char_segments() {
        assert_eq!(to_pascal_case("a_b_c"), "ABC");
        assert_eq!(to_pascal_case("x-y-z"), "XYZ");
    }

    /// Test that to_snake_case handles Unicode characters correctly
    #[test]
    fn test_to_snake_case_unicode() {
        assert_eq!(to_snake_case("CaféName"), "café_name");
        assert_eq!(to_snake_case("ÜberDriver"), "über_driver");
        assert_eq!(to_snake_case("ΑlphaΒeta"), "αlpha_βeta");
        assert_eq!(to_snake_case("MyProject🚀Name"), "my_project🚀_name");
        assert_eq!(to_snake_case("日本語Name"), "日本語_name");
    }

    /// Test that to_snake_case handles special characters
    #[test]
    fn test_to_snake_case_special_chars() {
        assert_eq!(to_snake_case("V8Engine"), "v8_engine");
        assert_eq!(to_snake_case("C++Parser"), "c++_parser");
        assert_eq!(to_snake_case("HTTPSConnection"), "httpsconnection");
        assert_eq!(to_snake_case("URLParser"), "urlparser");
        assert_eq!(to_snake_case("A"), "a");
        assert_eq!(to_snake_case("Z"), "z");
    }

    /// Test that to_snake_case handles boundary cases
    #[test]
    fn test_to_snake_case_boundaries() {
        assert_eq!(to_snake_case(""), "");
        assert_eq!(to_snake_case("a"), "a");
        assert_eq!(to_snake_case("A"), "a");
        assert_eq!(to_snake_case("alllowercase"), "alllowercase");
        assert_eq!(to_snake_case("ALLUPPERCASE"), "alluppercase");

        let mut long_name = "A".repeat(500);
        long_name.push_str(&"B".repeat(500));
        let result = to_snake_case(&long_name);
        assert!(result.len() > 500);
        assert!(result.chars().all(|c| c == 'a' || c == 'b'));
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn basic_property_stability(_input in ".*") {
            prop_assert!(true);
        }

        #[test]
        fn module_consistency_check(_x in 0u32..1000) {
            prop_assert!(_x < 1001);
        }
    }
}