//! Schema validation rules (AX-007, AX-008, AX-009)
//!
//! These rules validate JSON Schema completeness, detect conflicts across tools,
//! and enforce naming consistency.

use super::schema_helpers;
use super::{Rule, Severity, ValidationResult};
use crate::Result;
use crate::protocol::Tool;

/// AX-007: Schema Completeness
///
/// Every input property must have: type, description; required array must exist
pub struct SchemaCompletenessRule;

#[allow(clippy::unnecessary_literal_bound)]
impl Rule for SchemaCompletenessRule {
    fn code(&self) -> &str {
        "AX-007"
    }

    fn name(&self) -> &str {
        "Schema Completeness"
    }

    fn description(&self) -> &str {
        "Every input property must have type and description; required array must exist"
    }

    fn check(&self, tool: &Tool) -> Result<ValidationResult> {
        let mut result = ValidationResult::new(self.code(), self.name(), &tool.name);

        let properties = schema_helpers::input_properties(&tool.input_schema);

        let Some(props) = properties else {
            result.add_issue("No input properties defined");
            result.add_suggestion("Define input properties with type and description for each");
            return Ok(result.with_score(0.2).with_severity(Severity::Fail));
        };

        if props.is_empty() {
            result.passed = true;
            return Ok(result.with_score(1.0).with_severity(Severity::Pass));
        }

        let total = props.len();
        let mut missing_type = 0u32;
        let mut missing_desc = 0u32;

        for (name, prop) in props {
            let has_type = prop.get("type").is_some_and(|t| !t.is_null());
            let has_desc = prop
                .get("description")
                .is_some_and(|d| d.as_str().is_some_and(|s| !s.is_empty()));

            if !has_type {
                result.add_issue(format!("Property '{name}' missing 'type'"));
                missing_type += 1;
            }
            if !has_desc {
                result.add_issue(format!("Property '{name}' missing 'description'"));
                missing_desc += 1;
            }
        }

        // Check for required array
        let has_required = schema_helpers::has_required_array(&tool.input_schema);

        if !has_required {
            result.add_issue("Missing 'required' array in input schema");
            result.add_suggestion("Define which properties are required");
        }

        let missing_total = missing_type + missing_desc;
        #[allow(clippy::cast_precision_loss)]
        let completeness = if total > 0 {
            1.0 - (f64::from(missing_total) / (total as f64 * 2.0))
        } else {
            1.0
        };

        let score = if has_required {
            completeness
        } else {
            (completeness - 0.1).max(0.0)
        };

        let severity = if score < 0.5 {
            Severity::Fail
        } else if score < 0.8 {
            Severity::Warn
        } else {
            Severity::Pass
        };

        if !result.issues.is_empty() {
            result.add_suggestion("Add 'type' and 'description' to every input property");
        }

        result.passed = result.issues.is_empty();

        Ok(result.with_score(score).with_severity(severity))
    }
}

/// AX-008: Cross-Capability Conflict Detection
///
/// Detects duplicate tool names and overlapping functionality across tools.
/// This rule requires multi-tool context; for single-tool validation it passes.
pub struct ConflictDetectionRule;

#[allow(clippy::unnecessary_literal_bound)]
impl Rule for ConflictDetectionRule {
    fn code(&self) -> &str {
        "AX-008"
    }

    fn name(&self) -> &str {
        "Cross-Capability Conflict Detection"
    }

    fn description(&self) -> &str {
        "Detects duplicate tool names and overlapping functionality"
    }

    fn check(&self, tool: &Tool) -> Result<ValidationResult> {
        // Single-tool check: verify the tool name is reasonable for coexistence.
        // The multi-tool conflict detection is done in `check_conflicts`.
        let mut result = ValidationResult::new(self.code(), self.name(), &tool.name);

        // Check for overly generic names that are likely to conflict
        let generic_names = [
            "search", "query", "find", "get", "fetch", "send", "create", "update", "delete",
        ];

        if generic_names.contains(&tool.name.to_lowercase().as_str()) {
            result.add_issue(format!(
                "Name '{}' is too generic and likely to conflict with other tools",
                tool.name
            ));
            result.add_suggestion(
                "Use a service-prefixed name (e.g., 'brave_search' instead of 'search')",
            );
            return Ok(result.with_score(0.4).with_severity(Severity::Warn));
        }

        result.passed = true;
        Ok(result.with_score(1.0).with_severity(Severity::Pass))
    }
}

impl ConflictDetectionRule {
    /// Check for conflicts across multiple tools.
    ///
    /// Returns additional `ValidationResult` entries for any conflicts found.
    #[must_use]
    pub fn check_conflicts(tools: &[Tool]) -> Vec<ValidationResult> {
        let mut results = Vec::new();
        let mut seen_names: std::collections::HashMap<&str, usize> =
            std::collections::HashMap::new();

        // Detect duplicate names
        for (idx, tool) in tools.iter().enumerate() {
            if let Some(&prev_idx) = seen_names.get(tool.name.as_str()) {
                let mut result = ValidationResult::new(
                    "AX-008",
                    "Cross-Capability Conflict Detection",
                    &tool.name,
                );
                result.add_issue(format!(
                    "Duplicate tool name '{}' (also at index {prev_idx})",
                    tool.name
                ));
                result.add_suggestion("Rename one of the duplicates with a distinguishing prefix");
                results.push(result.with_score(0.0).with_severity(Severity::Fail));
            }
            seen_names.insert(&tool.name, idx);
        }

        // Detect overlapping functionality (tools with very similar names)
        for i in 0..tools.len() {
            for j in (i + 1)..tools.len() {
                let name_a = &tools[i].name;
                let name_b = &tools[j].name;

                // Check if one name is a substring of another (excluding prefix)
                let parts_a: Vec<&str> = name_a.split('_').collect();
                let parts_b: Vec<&str> = name_b.split('_').collect();

                // Same action verb with same service prefix suggests overlap
                if parts_a.len() >= 2
                    && parts_b.len() >= 2
                    && parts_a[0] == parts_b[0]
                    && parts_a.last() == parts_b.last()
                    && parts_a.len() != parts_b.len()
                {
                    let mut result = ValidationResult::new(
                        "AX-008",
                        "Cross-Capability Conflict Detection",
                        name_a,
                    );
                    result.add_issue(format!(
                        "Potential overlap: '{name_a}' and '{name_b}' share prefix and suffix"
                    ));
                    result
                        .add_suggestion("Consider merging or clearly differentiating these tools");
                    results.push(result.with_score(0.6).with_severity(Severity::Warn));
                }
            }
        }

        results
    }
}

/// AX-009: Naming Consistency
///
/// Enforces consistent naming patterns within a set of tools:
/// all should use the same separator style and follow a common convention.
pub struct NamingConsistencyRule;

#[allow(clippy::unnecessary_literal_bound)]
impl Rule for NamingConsistencyRule {
    fn code(&self) -> &str {
        "AX-009"
    }

    fn name(&self) -> &str {
        "Naming Consistency"
    }

    fn description(&self) -> &str {
        "Enforces consistent naming patterns across tools"
    }

    fn check(&self, tool: &Tool) -> Result<ValidationResult> {
        // Single-tool check: verify consistent internal naming convention
        let mut result = ValidationResult::new(self.code(), self.name(), &tool.name);

        let name = &tool.name;

        // Check for mixed separators within one name
        let has_underscore = name.contains('_');
        let has_dash = name.contains('-');
        let has_upper = name.chars().any(char::is_uppercase);

        let convention_count =
            usize::from(has_underscore) + usize::from(has_dash) + usize::from(has_upper);

        if convention_count > 1 {
            result.add_issue(format!(
                "Name '{name}' mixes naming conventions (found {})",
                [
                    has_underscore.then_some("snake_case"),
                    has_dash.then_some("kebab-case"),
                    has_upper.then_some("camelCase"),
                ]
                .into_iter()
                .flatten()
                .collect::<Vec<_>>()
                .join(", ")
            ));
            result.add_suggestion("Use consistent snake_case for tool names");
            return Ok(result.with_score(0.5).with_severity(Severity::Warn));
        }

        // Prefer snake_case
        if has_dash {
            result.add_issue(format!(
                "Name '{name}' uses kebab-case instead of snake_case"
            ));
            result.add_suggestion(
                "Use snake_case for MCP tool names (e.g., 'my_tool' not 'my-tool')",
            );
            return Ok(result.with_score(0.7).with_severity(Severity::Info));
        }

        if has_upper {
            result.add_issue(format!(
                "Name '{name}' uses camelCase instead of snake_case"
            ));
            result
                .add_suggestion("Use snake_case for MCP tool names (e.g., 'my_tool' not 'myTool')");
            return Ok(result.with_score(0.7).with_severity(Severity::Info));
        }

        result.passed = true;
        Ok(result.with_score(1.0).with_severity(Severity::Pass))
    }
}

impl NamingConsistencyRule {
    /// Check naming consistency across a set of tools.
    ///
    /// Returns additional `ValidationResult` entries for cross-tool inconsistencies.
    #[must_use]
    pub fn check_consistency(tools: &[Tool]) -> Vec<ValidationResult> {
        let mut results = Vec::new();

        if tools.len() < 2 {
            return results;
        }

        // Count naming conventions used across all tools
        let mut snake_count = 0usize;
        let mut kebab_count = 0usize;
        let mut camel_count = 0usize;

        for tool in tools {
            if tool.name.contains('_') {
                snake_count += 1;
            }
            if tool.name.contains('-') {
                kebab_count += 1;
            }
            if tool.name.chars().any(char::is_uppercase) {
                camel_count += 1;
            }
        }

        let conventions_used = usize::from(snake_count > 0)
            + usize::from(kebab_count > 0)
            + usize::from(camel_count > 0);

        if conventions_used > 1 {
            let mut result = ValidationResult::new("AX-009", "Naming Consistency", "(cross-tool)");
            result.add_issue(format!(
                "Mixed naming conventions across tools: {snake_count} snake_case, {kebab_count} kebab-case, {camel_count} camelCase"
            ));
            result.add_suggestion("Standardize all tool names to snake_case");
            results.push(result.with_score(0.4).with_severity(Severity::Warn));
        }

        results
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::protocol::Tool;
    use serde_json::json;

    fn create_tool(name: &str, description: &str, input_schema: serde_json::Value) -> Tool {
        Tool {
            name: name.to_string(),
            title: None,
            description: Some(description.to_string()),
            input_schema,
            output_schema: None,
            annotations: None,
            role: None,
            projection: None,
        }
    }

    // ── AX-007: Schema Completeness ──────────────────────────────

    #[test]
    fn schema_completeness_pass_all_properties_typed_and_described() {
        let rule = SchemaCompletenessRule;
        let tool = create_tool(
            "brave_search",
            "Search the web",
            json!({
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Search query"
                    },
                    "count": {
                        "type": "integer",
                        "description": "Number of results"
                    }
                },
                "required": ["query"]
            }),
        );

        let result = rule.check(&tool).unwrap();
        assert!(
            result.passed,
            "Expected pass, got issues: {:?}",
            result.issues
        );
        assert!(result.score > 0.8);
    }

    #[test]
    fn schema_completeness_fail_missing_type() {
        let rule = SchemaCompletenessRule;
        let tool = create_tool(
            "test_tool",
            "A tool",
            json!({
                "type": "object",
                "properties": {
                    "query": {
                        "description": "Search query"
                    }
                },
                "required": ["query"]
            }),
        );

        let result = rule.check(&tool).unwrap();
        assert!(!result.passed);
        assert!(result.issues.iter().any(|i| i.contains("missing 'type'")));
    }

    #[test]
    fn schema_completeness_fail_missing_description() {
        let rule = SchemaCompletenessRule;
        let tool = create_tool(
            "test_tool",
            "A tool",
            json!({
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string"
                    }
                },
                "required": ["query"]
            }),
        );

        let result = rule.check(&tool).unwrap();
        assert!(!result.passed);
        assert!(
            result
                .issues
                .iter()
                .any(|i| i.contains("missing 'description'"))
        );
    }

    #[test]
    fn schema_completeness_fail_no_required_array() {
        let rule = SchemaCompletenessRule;
        let tool = create_tool(
            "test_tool",
            "A tool",
            json!({
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Query"
                    }
                }
            }),
        );

        let result = rule.check(&tool).unwrap();
        assert!(!result.passed);
        assert!(result.issues.iter().any(|i| i.contains("required")));
    }

    #[test]
    fn schema_completeness_fail_no_properties() {
        let rule = SchemaCompletenessRule;
        let tool = create_tool("test_tool", "A tool", json!({"type": "object"}));

        let result = rule.check(&tool).unwrap();
        assert!(!result.passed);
        assert!(result.severity == Severity::Fail);
    }

    // ── AX-008: Conflict Detection ──────────────────────────────

    #[test]
    fn conflict_detection_pass_specific_name() {
        let rule = ConflictDetectionRule;
        let tool = create_tool(
            "brave_search",
            "Search the web via Brave",
            json!({"type": "object", "properties": {}}),
        );

        let result = rule.check(&tool).unwrap();
        assert!(result.passed);
        assert!(result.score > 0.9);
    }

    #[test]
    fn conflict_detection_warn_generic_name() {
        let rule = ConflictDetectionRule;
        let tool = create_tool(
            "search",
            "Search things",
            json!({"type": "object", "properties": {}}),
        );

        let result = rule.check(&tool).unwrap();
        assert!(!result.passed);
        assert!(result.severity == Severity::Warn);
    }

    #[test]
    fn conflict_detection_cross_tool_duplicate_names() {
        let tools = vec![
            create_tool(
                "brave_search",
                "Search A",
                json!({"type": "object", "properties": {}}),
            ),
            create_tool(
                "brave_search",
                "Search B",
                json!({"type": "object", "properties": {}}),
            ),
        ];

        let results = ConflictDetectionRule::check_conflicts(&tools);
        assert!(!results.is_empty());
        assert!(results[0].issues.iter().any(|i| i.contains("Duplicate")));
    }

    #[test]
    fn conflict_detection_cross_tool_no_duplicates() {
        let tools = vec![
            create_tool(
                "brave_search",
                "Search A",
                json!({"type": "object", "properties": {}}),
            ),
            create_tool(
                "google_search",
                "Search B",
                json!({"type": "object", "properties": {}}),
            ),
        ];

        let results = ConflictDetectionRule::check_conflicts(&tools);
        // No duplicates, possibly no overlap either
        assert!(
            results
                .iter()
                .all(|r| !r.issues.iter().any(|i| i.contains("Duplicate")))
        );
    }

    // ── AX-009: Naming Consistency ──────────────────────────────

    #[test]
    fn naming_consistency_pass_snake_case() {
        let rule = NamingConsistencyRule;
        let tool = create_tool(
            "brave_search_web",
            "Search",
            json!({"type": "object", "properties": {}}),
        );

        let result = rule.check(&tool).unwrap();
        assert!(result.passed);
        assert!(result.score > 0.9);
    }

    #[test]
    fn naming_consistency_warn_kebab_case() {
        let rule = NamingConsistencyRule;
        let tool = create_tool(
            "brave-search",
            "Search",
            json!({"type": "object", "properties": {}}),
        );

        let result = rule.check(&tool).unwrap();
        assert!(!result.passed);
        assert!(result.severity == Severity::Info);
    }

    #[test]
    fn naming_consistency_warn_mixed_conventions() {
        let rule = NamingConsistencyRule;
        let tool = create_tool(
            "brave_search-Web",
            "Search",
            json!({"type": "object", "properties": {}}),
        );

        let result = rule.check(&tool).unwrap();
        assert!(!result.passed);
        assert!(result.severity == Severity::Warn);
    }

    #[test]
    fn naming_consistency_cross_tool_mixed_conventions() {
        let tools = vec![
            create_tool(
                "brave_search",
                "A",
                json!({"type": "object", "properties": {}}),
            ),
            create_tool(
                "google-search",
                "B",
                json!({"type": "object", "properties": {}}),
            ),
        ];

        let results = NamingConsistencyRule::check_consistency(&tools);
        assert!(!results.is_empty());
        assert!(results[0].issues.iter().any(|i| i.contains("Mixed naming")));
    }

    #[test]
    fn naming_consistency_cross_tool_all_snake_case() {
        let tools = vec![
            create_tool(
                "brave_search",
                "A",
                json!({"type": "object", "properties": {}}),
            ),
            create_tool(
                "google_search",
                "B",
                json!({"type": "object", "properties": {}}),
            ),
        ];

        let results = NamingConsistencyRule::check_consistency(&tools);
        assert!(results.is_empty());
    }
}
