//! Plugin manifest validation (CC-PL-001 to CC-PL-015).
//!
//! Validates `.claude-plugin/plugin.json` manifests.

use crate::{
    config::PerFileLintConfig,
    diagnostics::{Diagnostic, Fix},
    rules::{Validator, ValidatorMetadata},
};
use rust_i18n::t;
use std::path::Path;

const RULE_IDS: &[&str] = &[
    "CC-PL-001",
    "CC-PL-002",
    "CC-PL-003",
    "CC-PL-004",
    "CC-PL-005",
    "CC-PL-006",
    "CC-PL-007",
    "CC-PL-008",
    "CC-PL-009",
    "CC-PL-010",
    "CC-PL-011",
    "CC-PL-012",
    "CC-PL-013",
    "CC-PL-014",
    "CC-PL-015",
];

pub struct PluginValidator;

impl Validator for PluginValidator {
    fn metadata(&self) -> ValidatorMetadata {
        ValidatorMetadata {
            name: self.name(),
            rule_ids: RULE_IDS,
        }
    }

    fn validate_per_file(
        &self,
        path: &Path,
        content: &str,
        config: &PerFileLintConfig<'_>,
    ) -> Vec<Diagnostic> {
        let mut diagnostics = Vec::new();

        if !config.rules().plugins {
            return diagnostics;
        }

        let plugin_dir = path.parent();
        let is_in_claude_plugin = plugin_dir
            .and_then(|p| p.file_name())
            .and_then(|n| n.to_str())
            .map(|n| n == ".claude-plugin")
            .unwrap_or(false);

        if config.is_rule_enabled("CC-PL-001") && !is_in_claude_plugin {
            diagnostics.push(
                Diagnostic::error(
                    path.to_path_buf(),
                    1,
                    0,
                    "CC-PL-001",
                    t!("rules.cc_pl_001.message"),
                )
                .with_suggestion(t!("rules.cc_pl_001.suggestion")),
            );
        }

        #[allow(clippy::collapsible_if)]
        if config.is_rule_enabled("CC-PL-002") && is_in_claude_plugin {
            if let Some(plugin_dir) = plugin_dir {
                let fs = config.fs();
                let disallowed = ["skills", "agents", "hooks", "commands"];
                for entry in disallowed {
                    if fs.exists(&plugin_dir.join(entry)) {
                        diagnostics.push(
                            Diagnostic::error(
                                path.to_path_buf(),
                                1,
                                0,
                                "CC-PL-002",
                                t!("rules.cc_pl_002.message", component = entry),
                            )
                            .with_suggestion(t!("rules.cc_pl_002.suggestion")),
                        );
                    }
                }
            }
        }

        let raw_value: serde_json::Value = match serde_json::from_str(content) {
            Ok(v) => v,
            Err(e) => {
                if config.is_rule_enabled("CC-PL-006") {
                    diagnostics.push(
                        Diagnostic::error(
                            path.to_path_buf(),
                            1,
                            0,
                            "CC-PL-006",
                            t!("rules.cc_pl_006.message", error = e.to_string()),
                        )
                        .with_suggestion(t!("rules.cc_pl_006.suggestion")),
                    );
                }
                return diagnostics;
            }
        };

        if config.is_rule_enabled("CC-PL-004") {
            check_required_field(&raw_value, "name", path, diagnostics.as_mut());
            check_recommended_field(&raw_value, "description", path, diagnostics.as_mut());
            check_recommended_field(&raw_value, "version", path, diagnostics.as_mut());
        }

        if config.is_rule_enabled("CC-PL-005") {
            if let Some(name) = raw_value.get("name").and_then(|v| v.as_str()) {
                if name.trim().is_empty() {
                    let mut diagnostic = Diagnostic::error(
                        path.to_path_buf(),
                        1,
                        0,
                        "CC-PL-005",
                        t!("rules.cc_pl_005.message"),
                    )
                    .with_suggestion(t!("rules.cc_pl_005.suggestion"));

                    // Unsafe auto-fix: populate empty plugin name with a deterministic placeholder.
                    if let Some((start, end, _)) =
                        find_unique_json_string_value_range(content, "name")
                    {
                        diagnostic = diagnostic.with_fix(Fix::replace(
                            start,
                            end,
                            "my-plugin",
                            "Set plugin name to 'my-plugin'",
                            false,
                        ));
                    }

                    diagnostics.push(diagnostic);
                }
            }
        }

        // CC-PL-007: Invalid component path / CC-PL-008: Component inside .claude-plugin
        let pl_007_enabled = config.is_rule_enabled("CC-PL-007");
        let pl_008_enabled = config.is_rule_enabled("CC-PL-008");
        if pl_007_enabled || pl_008_enabled {
            let path_fields = ["commands", "agents", "skills", "hooks"];
            for field in path_fields {
                if pl_007_enabled {
                    check_component_paths(&raw_value, field, path, content, &mut diagnostics);
                }
                if pl_008_enabled {
                    check_component_inside_claude_plugin(&raw_value, field, path, &mut diagnostics);
                }
            }
        }

        // CC-PL-015: A manifest path override shadows the matching default folder.
        if config.is_rule_enabled("CC-PL-015") && is_in_claude_plugin {
            check_default_component_shadowing(&raw_value, path, config, &mut diagnostics);
        }

        // CC-PL-009: Invalid author object
        if config.is_rule_enabled("CC-PL-009") {
            if let Some(author) = raw_value.get("author") {
                if author.is_object() {
                    let name_empty = author
                        .get("name")
                        .and_then(|n| n.as_str())
                        .map(|n| n.trim().is_empty())
                        .unwrap_or(true);
                    if name_empty {
                        diagnostics.push(
                            Diagnostic::warning(
                                path.to_path_buf(),
                                1,
                                0,
                                "CC-PL-009",
                                t!("rules.cc_pl_009.message"),
                            )
                            .with_suggestion(t!("rules.cc_pl_009.suggestion")),
                        );
                    }
                } else {
                    // author is present but not an object
                    diagnostics.push(
                        Diagnostic::warning(
                            path.to_path_buf(),
                            1,
                            0,
                            "CC-PL-009",
                            t!("rules.cc_pl_009.message"),
                        )
                        .with_suggestion(t!("rules.cc_pl_009.suggestion")),
                    );
                }
            }
        }

        // CC-PL-010: Invalid homepage URL
        if config.is_rule_enabled("CC-PL-010") {
            if let Some(homepage_val) = raw_value.get("homepage") {
                match homepage_val.as_str() {
                    Some(homepage) => {
                        if !homepage.is_empty() && !is_valid_url(homepage) {
                            diagnostics.push(
                                Diagnostic::warning(
                                    path.to_path_buf(),
                                    1,
                                    0,
                                    "CC-PL-010",
                                    t!("rules.cc_pl_010.message", url = homepage),
                                )
                                .with_suggestion(t!("rules.cc_pl_010.suggestion")),
                            );
                        }
                    }
                    None => {
                        // homepage is present but not a string (e.g., number, object)
                        let val_str = homepage_val.to_string();
                        diagnostics.push(
                            Diagnostic::warning(
                                path.to_path_buf(),
                                1,
                                0,
                                "CC-PL-010",
                                t!("rules.cc_pl_010.message", url = val_str.as_str()),
                            )
                            .with_suggestion(t!("rules.cc_pl_010.suggestion")),
                        );
                    }
                }
            }
        }

        if config.is_rule_enabled("CC-PL-003") {
            if let Some(version) = raw_value.get("version").and_then(|v| v.as_str()) {
                let trimmed = version.trim();
                if !trimmed.is_empty() && !is_valid_semver(trimmed) {
                    let mut diagnostic = Diagnostic::error(
                        path.to_path_buf(),
                        1,
                        0,
                        "CC-PL-003",
                        t!("rules.cc_pl_003.message", version = version),
                    )
                    .with_suggestion(t!("rules.cc_pl_003.suggestion"));

                    // Normalize partial semver: "1" -> "1.0.0", "1.2" -> "1.2.0"
                    let normalized = normalize_partial_semver(trimmed);
                    if let Some(norm) = &normalized {
                        if norm != trimmed {
                            if let Some((start, end)) =
                                crate::rules::find_unique_json_string_value_span(
                                    content, "version", version,
                                )
                            {
                                diagnostic = diagnostic.with_fix(Fix::replace(
                                    start,
                                    end,
                                    norm.as_str(),
                                    format!("Normalize version to '{}'", norm),
                                    false,
                                ));
                            }
                        }
                    }

                    diagnostics.push(diagnostic);
                }
            }
        }

        // CC-PL-011: LSP server missing required fields
        if config.is_rule_enabled("CC-PL-011") {
            if let Some(lsp_servers) = raw_value.get("lspServers") {
                if let Some(obj) = lsp_servers.as_object() {
                    for (server_name, server_val) in obj {
                        if let Some(server_obj) = server_val.as_object() {
                            if !server_obj.contains_key("command") {
                                diagnostics.push(
                                    Diagnostic::error(
                                        path.to_path_buf(),
                                        1,
                                        0,
                                        "CC-PL-011",
                                        format!(
                                            "LSP server '{}' is missing required field 'command'",
                                            server_name
                                        ),
                                    )
                                    .with_suggestion(format!(
                                        "Add a 'command' string to LSP server '{}'",
                                        server_name
                                    )),
                                );
                            }
                            if !server_obj.contains_key("extensionToLanguage") {
                                diagnostics.push(
                                    Diagnostic::error(
                                        path.to_path_buf(),
                                        1,
                                        0,
                                        "CC-PL-011",
                                        format!(
                                            "LSP server '{}' is missing required field 'extensionToLanguage'",
                                            server_name
                                        ),
                                    )
                                    .with_suggestion(format!(
                                        "Add an 'extensionToLanguage' mapping to LSP server '{}'",
                                        server_name
                                    )),
                                );
                            }
                        }
                    }
                }
            }
        }

        // CC-PL-012: Invalid userConfig key
        if config.is_rule_enabled("CC-PL-012") {
            if let Some(user_config) = raw_value.get("userConfig") {
                if let Some(obj) = user_config.as_object() {
                    for key in obj.keys() {
                        if !is_valid_identifier(key) {
                            diagnostics.push(
                                Diagnostic::warning(
                                    path.to_path_buf(),
                                    1,
                                    0,
                                    "CC-PL-012",
                                    format!(
                                        "Invalid userConfig key '{}': must be a valid identifier (alphanumeric and underscores, not starting with a number)",
                                        key
                                    ),
                                )
                                .with_suggestion(format!(
                                    "Rename '{}' to a valid identifier (e.g., 'my_config_key')",
                                    key
                                )),
                            );
                        }
                    }
                }
            }
        }

        // CC-PL-013: channels entry missing server field
        if config.is_rule_enabled("CC-PL-013") {
            if let Some(channels) = raw_value.get("channels") {
                if let Some(arr) = channels.as_array() {
                    let mcp_server_keys: Vec<String> = raw_value
                        .get("mcpServers")
                        .and_then(|v| v.as_object())
                        .map(|obj| obj.keys().cloned().collect())
                        .unwrap_or_default();

                    for (i, entry) in arr.iter().enumerate() {
                        match entry.get("server").and_then(|v| v.as_str()) {
                            None => {
                                diagnostics.push(
                                    Diagnostic::warning(
                                        path.to_path_buf(),
                                        1,
                                        0,
                                        "CC-PL-013",
                                        format!(
                                            "channels[{}] is missing required 'server' field",
                                            i
                                        ),
                                    )
                                    .with_suggestion(
                                        "Add a 'server' field referencing a key in 'mcpServers'"
                                            .to_string(),
                                    ),
                                );
                            }
                            Some(server_ref) => {
                                if !mcp_server_keys.is_empty()
                                    && !mcp_server_keys.contains(&server_ref.to_string())
                                {
                                    diagnostics.push(
                                        Diagnostic::warning(
                                            path.to_path_buf(),
                                            1,
                                            0,
                                            "CC-PL-013",
                                            format!(
                                                "channels[{}] references server '{}' which is not defined in 'mcpServers'",
                                                i, server_ref
                                            ),
                                        )
                                        .with_suggestion(format!(
                                            "Add '{}' to 'mcpServers' or fix the server reference",
                                            server_ref
                                        )),
                                    );
                                }
                            }
                        }
                    }
                }
            }
        }

        // CC-PL-014: Plugin agent uses unsupported field
        if config.is_rule_enabled("CC-PL-014") {
            if let Some(agents) = raw_value.get("agents") {
                let unsupported_fields = ["hooks", "mcpServers", "permissionMode"];
                let agent_entries: Vec<(&str, &serde_json::Value)> =
                    if let Some(obj) = agents.as_object() {
                        obj.iter().map(|(k, v)| (k.as_str(), v)).collect()
                    } else if let Some(arr) = agents.as_array() {
                        arr.iter()
                            .map(|v| {
                                let name = v.get("name").and_then(|n| n.as_str()).unwrap_or("");
                                (name, v)
                            })
                            .collect()
                    } else {
                        vec![]
                    };

                for (agent_name, agent_val) in agent_entries {
                    if let Some(agent_obj) = agent_val.as_object() {
                        for field in unsupported_fields {
                            if agent_obj.contains_key(field) {
                                let label = if agent_name.is_empty() {
                                    "Plugin agent".to_string()
                                } else {
                                    format!("Plugin agent '{}'", agent_name)
                                };
                                diagnostics.push(
                                    Diagnostic::warning(
                                        path.to_path_buf(),
                                        1,
                                        0,
                                        "CC-PL-014",
                                        format!(
                                            "{} uses unsupported field '{}' (ignored for plugin agents)",
                                            label, field
                                        ),
                                    )
                                    .with_suggestion(format!(
                                        "Remove '{}' from the plugin agent definition",
                                        field
                                    )),
                                );
                            }
                        }
                    }
                }
            }
        }

        diagnostics
    }
}

fn is_field_missing(value: &serde_json::Value, field: &str) -> bool {
    match value.get(field) {
        Some(v) => !v.is_string() || v.as_str().map(|s| s.trim().is_empty()).unwrap_or(true),
        None => true,
    }
}

fn check_required_field(
    value: &serde_json::Value,
    field: &str,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if is_field_missing(value, field) {
        diagnostics.push(
            Diagnostic::error(
                path.to_path_buf(),
                1,
                0,
                "CC-PL-004",
                t!("rules.cc_pl_004.message", field = field),
            )
            .with_suggestion(t!("rules.cc_pl_004.suggestion", field = field)),
        );
    }
}

fn check_recommended_field(
    value: &serde_json::Value,
    field: &str,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if is_field_missing(value, field) {
        diagnostics.push(
            Diagnostic::warning(
                path.to_path_buf(),
                1,
                0,
                "CC-PL-004",
                t!("rules.cc_pl_004_recommended.message", field = field),
            )
            .with_suggestion(t!("rules.cc_pl_004_recommended.suggestion", field = field)),
        );
    }
}

fn is_valid_semver(version: &str) -> bool {
    semver::Version::parse(version).is_ok()
}

/// Normalize partial semver strings: "1" -> "1.0.0", "1.2" -> "1.2.0"
fn normalize_partial_semver(version: &str) -> Option<String> {
    let parts: Vec<&str> = version.split('.').collect();
    match parts.len() {
        1 => {
            if parts[0].chars().all(|c| c.is_ascii_digit()) {
                Some(format!("{}.0.0", parts[0]))
            } else {
                None
            }
        }
        2 => {
            if parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit())) {
                Some(format!("{}.{}.0", parts[0], parts[1]))
            } else {
                None
            }
        }
        _ => None,
    }
}

/// Find a unique string value span for a JSON key.
/// Returns (value_start, value_end, value_content_without_quotes).
fn find_unique_json_string_value_range(content: &str, key: &str) -> Option<(usize, usize, String)> {
    crate::span_utils::find_unique_json_string_value_range(content, key)
}

/// Check if a path string is invalid for a component path.
/// Must be relative (no absolute paths), must not use `..` traversal.
fn is_invalid_component_path(p: &str) -> bool {
    let trimmed = p.trim();
    if trimmed.is_empty() {
        return false;
    }
    // Absolute paths: starts with `/` or `\`
    if trimmed.starts_with('/') || trimmed.starts_with('\\') {
        return true;
    }
    // Windows drive letter paths: C:\... or C:/...
    if trimmed.len() >= 2 {
        let bytes = trimmed.as_bytes();
        if bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
            return true;
        }
    }
    // Check for `..` traversal in any component (split on both / and \)
    trimmed.split(['/', '\\']).any(|part| part == "..")
}

/// Check if a path is a relative path missing a `./` prefix (autofixable).
/// This is separate from `is_invalid_component_path`: paths like `skills/foo`
/// are not absolute or traversal, but should have `./` prepended.
fn is_autofixable_path(p: &str) -> bool {
    let trimmed = p.trim();
    // Must not be empty, not already have ./ prefix, and not be invalid
    !trimmed.is_empty()
        && !trimmed.starts_with("./")
        && !trimmed.starts_with(".\\")
        && !is_invalid_component_path(trimmed)
}

/// Check if a path starts with `.claude-plugin/`.
/// Normalizes an optional leading `./` or `.\\` before checking.
fn path_inside_claude_plugin(p: &str) -> bool {
    let trimmed = p.trim();
    let normalized = trimmed
        .strip_prefix("./")
        .or_else(|| trimmed.strip_prefix(".\\"))
        .unwrap_or(trimmed);
    normalized.starts_with(".claude-plugin/")
        || normalized.starts_with(".claude-plugin\\")
        || normalized == ".claude-plugin"
}

/// Extract string paths from a JSON value that can be a string or array of strings.
fn extract_paths(value: &serde_json::Value) -> Vec<String> {
    match value {
        serde_json::Value::String(s) => vec![s.clone()],
        serde_json::Value::Array(arr) => arr
            .iter()
            .filter_map(|v| v.as_str().map(String::from))
            .collect(),
        _ => vec![],
    }
}

fn path_points_to_default_component(p: &str, component: &str) -> bool {
    let normalized = p
        .trim()
        .replace('\\', "/")
        .trim_start_matches("./")
        .trim_end_matches('/')
        .to_string();
    normalized == component
}

fn extract_valid_shadowing_paths(value: &serde_json::Value) -> Option<Vec<String>> {
    match value {
        serde_json::Value::String(s) => Some(vec![s.clone()]),
        serde_json::Value::Array(arr) => arr.iter().map(|v| v.as_str().map(String::from)).collect(),
        _ => None,
    }
}

/// CC-PL-015: Detect root default component folders shadowed by manifest paths.
fn check_default_component_shadowing(
    raw_value: &serde_json::Value,
    path: &Path,
    config: &PerFileLintConfig<'_>,
    diagnostics: &mut Vec<Diagnostic>,
) {
    let Some(plugin_dir) = path.parent() else {
        return;
    };
    let Some(plugin_root) = plugin_dir.parent() else {
        return;
    };

    let fs = config.fs();
    for component in ["commands", "agents", "skills", "hooks"] {
        let Some(manifest_value) = raw_value.get(component) else {
            continue;
        };
        if !fs.is_dir(&plugin_root.join(component)) {
            continue;
        }

        let Some(manifest_paths) = extract_valid_shadowing_paths(manifest_value) else {
            continue;
        };

        let includes_default = manifest_paths
            .iter()
            .any(|p| path_points_to_default_component(p, component));
        if includes_default {
            continue;
        }

        diagnostics.push(
            Diagnostic::warning(
                path.to_path_buf(),
                1,
                0,
                "CC-PL-015",
                t!("rules.cc_pl_015.message", component = component),
            )
            .with_suggestion(t!("rules.cc_pl_015.suggestion", component = component)),
        );
    }
}

/// CC-PL-007: Validate component paths are relative without `..` traversal.
/// Also flags relative paths missing a `./` prefix (with safe autofix).
fn check_component_paths(
    raw_value: &serde_json::Value,
    field: &str,
    path: &Path,
    content: &str,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if let Some(val) = raw_value.get(field) {
        for p in extract_paths(val) {
            if is_invalid_component_path(&p) {
                // Absolute or traversal path: error without autofix
                diagnostics.push(
                    Diagnostic::error(
                        path.to_path_buf(),
                        1,
                        0,
                        "CC-PL-007",
                        t!("rules.cc_pl_007.message", field = field, path = p.as_str()),
                    )
                    .with_suggestion(t!("rules.cc_pl_007.suggestion")),
                );
            } else if is_autofixable_path(&p) {
                // Relative path missing ./ prefix: error with safe autofix
                let mut diagnostic = Diagnostic::error(
                    path.to_path_buf(),
                    1,
                    0,
                    "CC-PL-007",
                    t!("rules.cc_pl_007.message", field = field, path = p.as_str()),
                )
                .with_suggestion(t!("rules.cc_pl_007.suggestion"));

                if let Some((start, end, _)) = find_unique_json_string_value_range(content, field) {
                    let fixed = format!("./{}", p.trim());
                    diagnostic = diagnostic.with_fix(Fix::replace(
                        start,
                        end,
                        &fixed,
                        format!("Prepend './' to path: '{}'", p.trim()),
                        true,
                    ));
                }

                diagnostics.push(diagnostic);
            }
        }
    }
}

/// CC-PL-008: Detect component paths pointing inside .claude-plugin/.
fn check_component_inside_claude_plugin(
    raw_value: &serde_json::Value,
    field: &str,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if let Some(val) = raw_value.get(field) {
        for p in extract_paths(val) {
            if path_inside_claude_plugin(&p) {
                diagnostics.push(
                    Diagnostic::error(
                        path.to_path_buf(),
                        1,
                        0,
                        "CC-PL-008",
                        t!("rules.cc_pl_008.message", field = field, path = p.as_str()),
                    )
                    .with_suggestion(t!("rules.cc_pl_008.suggestion")),
                );
            }
        }
    }
}

/// Check if a URL is valid (http or https scheme).
fn is_valid_url(url: &str) -> bool {
    url.starts_with("http://") || url.starts_with("https://")
}

/// Check if a string is a valid identifier (alphanumeric + underscores, not starting with a number).
fn is_valid_identifier(s: &str) -> bool {
    if s.is_empty() {
        return false;
    }
    let mut chars = s.chars();
    match chars.next() {
        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
        _ => return false,
    }
    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::LintConfig;
    use std::fs;
    use tempfile::TempDir;

    fn write_plugin(path: &Path, content: &str) {
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        fs::write(path, content).unwrap();
    }

    #[test]
    fn test_cc_pl_001_manifest_not_in_claude_plugin() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test-plugin","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-001"));
    }

    #[test]
    fn test_cc_pl_002_components_in_claude_plugin() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test-plugin","description":"desc","version":"1.0.0"}"#,
        );
        fs::create_dir_all(temp.path().join(".claude-plugin").join("skills")).unwrap();

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-002"));
    }

    #[test]
    fn test_cc_pl_003_invalid_semver() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test-plugin","description":"desc","version":"1.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-003"));
    }

    #[test]
    fn test_cc_pl_003_has_fix() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        let content = r#"{"name":"test-plugin","description":"desc","version":"1.0"}"#;
        write_plugin(&plugin_path, content);

        let validator = PluginValidator;
        let diagnostics = validator.validate(&plugin_path, content, &LintConfig::default());

        let cc_pl_003: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-003")
            .collect();
        assert_eq!(cc_pl_003.len(), 1);
        assert!(
            cc_pl_003[0].has_fixes(),
            "CC-PL-003 should have auto-fix for partial semver"
        );
        let fix = &cc_pl_003[0].fixes[0];
        assert!(!fix.safe, "CC-PL-003 fix should be unsafe");
        assert_eq!(
            fix.replacement, "1.0.0",
            "Fix should normalize '1.0' to '1.0.0'"
        );
    }

    #[test]
    fn test_cc_pl_003_valid_prerelease_version() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test-plugin","description":"desc","version":"4.0.0-rc.1"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-003"));
    }

    #[test]
    fn test_cc_pl_003_valid_build_metadata() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test-plugin","description":"desc","version":"1.0.0+build.123"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-003"));
    }

    #[test]
    fn test_cc_pl_003_skips_empty_version() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test-plugin","description":"desc","version":""}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_004 = diagnostics
            .iter()
            .find(|d| d.rule == "CC-PL-004")
            .expect("CC-PL-004 should be reported for empty version");
        assert_eq!(
            pl_004.level,
            crate::diagnostics::DiagnosticLevel::Warning,
            "Empty version should be a warning, not an error"
        );
        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-003"));
    }

    #[test]
    fn test_cc_pl_004_missing_recommended_fields() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, r#"{"name":"test-plugin"}"#);

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_004: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-004")
            .collect();
        assert_eq!(
            pl_004.len(),
            2,
            "Should warn for missing description and version"
        );
        for d in &pl_004 {
            assert_eq!(
                d.level,
                crate::diagnostics::DiagnosticLevel::Warning,
                "Missing description/version should be warnings, not errors"
            );
        }
    }

    #[test]
    fn test_cc_pl_005_empty_name() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"  ","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let cc_pl_005 = diagnostics
            .iter()
            .find(|d| d.rule == "CC-PL-005")
            .expect("CC-PL-005 should be reported");
        assert!(cc_pl_005.has_fixes());
        let fix = &cc_pl_005.fixes[0];
        assert_eq!(fix.replacement, "my-plugin");
        assert!(!fix.safe);
    }

    // ===== CC-PL-006: Plugin Parse Error =====

    #[test]
    fn test_cc_pl_006_invalid_json() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, r#"{ invalid json }"#);

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let parse_errors: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-006")
            .collect();
        assert_eq!(parse_errors.len(), 1);
        assert!(parse_errors[0].message.contains("Failed to parse"));
    }

    #[test]
    fn test_cc_pl_006_truncated_json() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, r#"{"name":"test"#);

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-006"));
    }

    #[test]
    fn test_cc_pl_006_empty_file() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, "");

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-006"));
    }

    #[test]
    fn test_cc_pl_006_valid_json_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-006"));
    }

    #[test]
    fn test_cc_pl_006_disabled() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, r#"{ invalid }"#);

        let mut config = LintConfig::default();
        config.rules_mut().disabled_rules = vec!["CC-PL-006".to_string()];

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &config,
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-006"));
    }

    // ===== Additional edge case tests =====

    #[test]
    fn test_cc_pl_001_valid_location_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-001"));
    }

    #[test]
    fn test_cc_pl_001_disabled() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let mut config = LintConfig::default();
        config.rules_mut().disabled_rules = vec!["CC-PL-001".to_string()];

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &config,
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-001"));
    }

    #[test]
    fn test_cc_pl_002_no_components_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );
        // No skills/agents/hooks/commands directories

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-002"));
    }

    #[test]
    fn test_cc_pl_002_multiple_components() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );
        // Create multiple disallowed directories
        fs::create_dir_all(temp.path().join(".claude-plugin").join("skills")).unwrap();
        fs::create_dir_all(temp.path().join(".claude-plugin").join("agents")).unwrap();
        fs::create_dir_all(temp.path().join(".claude-plugin").join("hooks")).unwrap();
        fs::create_dir_all(temp.path().join(".claude-plugin").join("commands")).unwrap();

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_002_errors: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-002")
            .collect();
        assert_eq!(pl_002_errors.len(), 4);
    }

    #[test]
    fn test_cc_pl_004_all_fields_present_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"A test plugin","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-004"));
    }

    #[test]
    fn test_cc_pl_004_empty_string_values() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"","version":""}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_004_warnings: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-004")
            .collect();
        // Both description and version are empty - reported as warnings
        assert_eq!(pl_004_warnings.len(), 2);
        for d in &pl_004_warnings {
            assert_eq!(
                d.level,
                crate::diagnostics::DiagnosticLevel::Warning,
                "Empty description/version should be warnings"
            );
        }
    }

    #[test]
    fn test_cc_pl_004_missing_name_is_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, r#"{"description":"d","version":"1.0.0"}"#);

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let name_error = diagnostics
            .iter()
            .find(|d| {
                d.rule == "CC-PL-004" && d.level == crate::diagnostics::DiagnosticLevel::Error
            })
            .expect("CC-PL-004 error should be reported for missing name");
        assert!(
            name_error.message.contains("name"),
            "Error message should mention 'name'"
        );
    }

    #[test]
    fn test_cc_pl_004_only_name_present_no_errors() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, r#"{"name":"test"}"#);

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        // No CC-PL-004 errors - only warnings for missing description/version
        assert!(
            !diagnostics
                .iter()
                .any(|d| d.rule == "CC-PL-004"
                    && d.level == crate::diagnostics::DiagnosticLevel::Error),
            "With name present, there should be zero CC-PL-004 errors"
        );

        let warnings: Vec<_> = diagnostics
            .iter()
            .filter(|d| {
                d.rule == "CC-PL-004" && d.level == crate::diagnostics::DiagnosticLevel::Warning
            })
            .collect();
        assert_eq!(
            warnings.len(),
            2,
            "Should have warnings for missing description and version"
        );
        assert!(
            warnings.iter().any(|d| d.message.contains("description")),
            "Should mention 'description' in warning"
        );
        assert!(
            warnings.iter().any(|d| d.message.contains("version")),
            "Should mention 'version' in warning"
        );
    }

    #[test]
    fn test_cc_pl_004_non_string_name_is_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":123,"description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let name_error = diagnostics.iter().find(|d| {
            d.rule == "CC-PL-004" && d.level == crate::diagnostics::DiagnosticLevel::Error
        });
        assert!(
            name_error.is_some(),
            "Non-string name should trigger CC-PL-004 error"
        );
    }

    #[test]
    fn test_cc_pl_004_non_string_recommended_fields_are_warnings() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":123,"version":true}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let warnings: Vec<_> = diagnostics
            .iter()
            .filter(|d| {
                d.rule == "CC-PL-004" && d.level == crate::diagnostics::DiagnosticLevel::Warning
            })
            .collect();
        assert_eq!(
            warnings.len(),
            2,
            "Non-string description and version should trigger warnings"
        );
    }

    #[test]
    fn test_cc_pl_003_skips_when_version_absent() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, r#"{"name":"test"}"#);

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(
            !diagnostics.iter().any(|d| d.rule == "CC-PL-003"),
            "CC-PL-003 should not fire when version is absent"
        );
    }

    #[test]
    fn test_cc_pl_003_fires_despite_non_string_description() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":123,"version":"1.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(
            diagnostics.iter().any(|d| d.rule == "CC-PL-003"),
            "CC-PL-003 should fire for invalid semver even with non-string description"
        );
        assert!(
            diagnostics.iter().any(|d| d.rule == "CC-PL-004"
                && d.level == crate::diagnostics::DiagnosticLevel::Warning),
            "CC-PL-004 warning should fire for non-string description"
        );
    }

    #[test]
    fn test_cc_pl_004_disabled_via_config() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, r#"{}"#);

        let mut config = LintConfig::default();
        config.rules_mut().disabled_rules = vec!["CC-PL-004".to_string()];

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &config,
        );

        assert!(
            !diagnostics.iter().any(|d| d.rule == "CC-PL-004"),
            "CC-PL-004 should not fire when disabled"
        );
    }

    #[test]
    fn test_cc_pl_004_whitespace_only_name_is_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"   ","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(
            diagnostics
                .iter()
                .any(|d| d.rule == "CC-PL-004"
                    && d.level == crate::diagnostics::DiagnosticLevel::Error),
            "Whitespace-only name should trigger CC-PL-004 error"
        );
    }

    #[test]
    fn test_cc_pl_004_whitespace_only_recommended_fields() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"  ","version":"  "}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let warnings: Vec<_> = diagnostics
            .iter()
            .filter(|d| {
                d.rule == "CC-PL-004" && d.level == crate::diagnostics::DiagnosticLevel::Warning
            })
            .collect();
        assert_eq!(
            warnings.len(),
            2,
            "Whitespace-only description and version should trigger warnings"
        );
    }

    #[test]
    fn test_cc_pl_005_non_empty_name_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"my-plugin","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-005"));
    }

    #[test]
    fn test_config_disabled_plugins_category() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join("plugin.json");
        write_plugin(&plugin_path, r#"{ invalid json }"#);

        let mut config = LintConfig::default();
        config.rules_mut().plugins = false;

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &config,
        );

        assert!(diagnostics.is_empty());
    }

    // ===== CC-PL-007: Invalid Component Path =====

    #[test]
    fn test_cc_pl_007_absolute_path() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":"/usr/local/bin/cmd"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-007"));
    }

    #[test]
    fn test_cc_pl_007_windows_absolute_path() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","skills":"C:\\Users\\skills"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-007"));
    }

    #[test]
    fn test_cc_pl_007_traversal_path() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","agents":"../outside/agents"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-007"));
    }

    #[test]
    fn test_cc_pl_007_embedded_traversal() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","hooks":"./valid/../escape"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-007"));
    }

    #[test]
    fn test_cc_pl_007_array_of_paths() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","skills":["./valid","../invalid"]}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_007: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-007")
            .collect();
        assert_eq!(
            pl_007.len(),
            1,
            "Only the invalid path should trigger CC-PL-007"
        );
    }

    #[test]
    fn test_cc_pl_007_valid_relative_path_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":"./commands/"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-007"));
    }

    #[test]
    fn test_cc_pl_007_no_path_fields_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-007"));
    }

    #[test]
    fn test_cc_pl_007_disabled() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":"/absolute"}"#,
        );

        let mut config = LintConfig::default();
        config.rules_mut().disabled_rules = vec!["CC-PL-007".to_string()];

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &config,
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-007"));
    }

    // ===== CC-PL-008: Component Inside .claude-plugin =====

    #[test]
    fn test_cc_pl_008_path_inside_claude_plugin() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","agents":".claude-plugin/agents"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-008"));
    }

    #[test]
    fn test_cc_pl_008_array_with_mixed_paths() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","skills":["./valid",".claude-plugin/invalid"]}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-008"));
    }

    #[test]
    fn test_cc_pl_008_valid_path_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","skills":"./skills/"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-008"));
    }

    #[test]
    fn test_cc_pl_008_disabled() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","agents":".claude-plugin/agents"}"#,
        );

        let mut config = LintConfig::default();
        config.rules_mut().disabled_rules = vec!["CC-PL-008".to_string()];

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &config,
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-008"));
    }

    // ===== CC-PL-009: Invalid Author Object =====

    #[test]
    fn test_cc_pl_009_empty_author_name() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","author":{"name":""}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-009"));
    }

    #[test]
    fn test_cc_pl_009_whitespace_author_name() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","author":{"name":"  "}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-009"));
    }

    #[test]
    fn test_cc_pl_009_missing_author_name() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","author":{"email":"a@b.com"}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-009"));
    }

    #[test]
    fn test_cc_pl_009_author_not_object() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","author":"just a string"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-009"));
    }

    #[test]
    fn test_cc_pl_009_valid_author_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","author":{"name":"Test Author"}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-009"));
    }

    #[test]
    fn test_cc_pl_009_no_author_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-009"));
    }

    #[test]
    fn test_cc_pl_009_disabled() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","author":{"name":""}}"#,
        );

        let mut config = LintConfig::default();
        config.rules_mut().disabled_rules = vec!["CC-PL-009".to_string()];

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &config,
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-009"));
    }

    // ===== CC-PL-010: Invalid Homepage URL =====

    #[test]
    fn test_cc_pl_010_invalid_url() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","homepage":"not-a-url"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-010"));
    }

    #[test]
    fn test_cc_pl_010_ftp_url() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","homepage":"ftp://example.com"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-010"));
    }

    #[test]
    fn test_cc_pl_010_valid_https_url_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","homepage":"https://example.com"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-010"));
    }

    #[test]
    fn test_cc_pl_010_valid_http_url_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","homepage":"http://example.com"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-010"));
    }

    #[test]
    fn test_cc_pl_010_no_homepage_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-010"));
    }

    #[test]
    fn test_cc_pl_010_empty_homepage_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","homepage":""}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-010"));
    }

    #[test]
    fn test_cc_pl_010_disabled() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","homepage":"not-a-url"}"#,
        );

        let mut config = LintConfig::default();
        config.rules_mut().disabled_rules = vec!["CC-PL-010".to_string()];

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &config,
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-010"));
    }

    // ===== Review feedback tests =====

    #[test]
    fn test_cc_pl_007_windows_forward_slash_absolute() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":"C:/Users/skills"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(
            diagnostics.iter().any(|d| d.rule == "CC-PL-007"),
            "C:/ forward-slash Windows paths should be detected"
        );
    }

    #[test]
    fn test_cc_pl_007_trailing_traversal() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","hooks":"./foo/.."}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(
            diagnostics.iter().any(|d| d.rule == "CC-PL-007"),
            "Trailing /.. should be detected as traversal"
        );
    }

    #[test]
    fn test_cc_pl_007_mixed_slash_traversal() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","agents":"./foo/..\\bar"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(
            diagnostics.iter().any(|d| d.rule == "CC-PL-007"),
            "Mixed slash traversal should be detected"
        );
    }

    #[test]
    fn test_cc_pl_007_autofixable_path() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":"commands/run"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_007: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-007")
            .collect();
        assert_eq!(
            pl_007.len(),
            1,
            "Missing ./ prefix should trigger CC-PL-007"
        );
        assert!(!pl_007[0].fixes.is_empty(), "Should have a safe autofix");
        assert!(pl_007[0].fixes[0].safe, "Autofix should be safe");
        assert!(
            pl_007[0].fixes[0].replacement.starts_with("./"),
            "Autofix should prepend ./"
        );
    }

    #[test]
    fn test_cc_pl_008_dot_slash_prefix_bypass() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","agents":"./.claude-plugin/agents"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(
            diagnostics.iter().any(|d| d.rule == "CC-PL-008"),
            "./.claude-plugin/ should still be detected"
        );
    }

    #[test]
    fn test_cc_pl_010_non_string_homepage() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","homepage":123}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(
            diagnostics.iter().any(|d| d.rule == "CC-PL-010"),
            "Non-string homepage should trigger CC-PL-010"
        );
    }

    // ===== CC-PL-006 suggestion test =====

    #[test]
    fn test_cc_pl_006_has_suggestion() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(&plugin_path, r#"{ invalid json }"#);

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let cc_pl_006: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-006")
            .collect();
        assert_eq!(cc_pl_006.len(), 1);
        assert!(
            cc_pl_006[0].suggestion.is_some(),
            "CC-PL-006 should have a suggestion"
        );
        assert!(
            cc_pl_006[0]
                .suggestion
                .as_ref()
                .unwrap()
                .contains("Validate JSON syntax"),
            "CC-PL-006 suggestion should mention JSON syntax"
        );
    }

    // ===== CC-PL-011: LSP Server Missing Required Fields =====

    #[test]
    fn test_cc_pl_011_missing_command() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","lspServers":{"myServer":{"extensionToLanguage":{".rs":"rust"}}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_011: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-011")
            .collect();
        assert_eq!(pl_011.len(), 1);
        assert!(
            pl_011[0].message.contains("command"),
            "Should mention missing 'command'"
        );
    }

    #[test]
    fn test_cc_pl_011_missing_extension_to_language() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","lspServers":{"myServer":{"command":"rust-analyzer"}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_011: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-011")
            .collect();
        assert_eq!(pl_011.len(), 1);
        assert!(
            pl_011[0].message.contains("extensionToLanguage"),
            "Should mention missing 'extensionToLanguage'"
        );
    }

    #[test]
    fn test_cc_pl_011_missing_both_fields() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","lspServers":{"myServer":{}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_011: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-011")
            .collect();
        assert_eq!(pl_011.len(), 2, "Should report both missing fields");
    }

    #[test]
    fn test_cc_pl_011_valid_lsp_server_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","lspServers":{"myServer":{"command":"rust-analyzer","extensionToLanguage":{".rs":"rust"}}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-011"));
    }

    #[test]
    fn test_cc_pl_011_no_lsp_servers_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-011"));
    }

    // ===== CC-PL-012: Invalid userConfig Key =====

    #[test]
    fn test_cc_pl_012_key_starts_with_number() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","userConfig":{"1invalid":{"type":"string"}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_012: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-012")
            .collect();
        assert_eq!(pl_012.len(), 1);
        assert!(
            pl_012[0].message.contains("1invalid"),
            "Should mention the invalid key"
        );
    }

    #[test]
    fn test_cc_pl_012_key_with_special_chars() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","userConfig":{"my-key":{"type":"string"},"my.key":{"type":"number"}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_012: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-012")
            .collect();
        assert_eq!(
            pl_012.len(),
            2,
            "Both keys with special characters should be flagged"
        );
    }

    #[test]
    fn test_cc_pl_012_valid_keys_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","userConfig":{"valid_key":{},"_also_valid":{},"camelCase123":{}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-012"));
    }

    #[test]
    fn test_cc_pl_012_no_user_config_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-012"));
    }

    // ===== CC-PL-013: Channels Entry Missing Server Field =====

    #[test]
    fn test_cc_pl_013_missing_server() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","mcpServers":{"myMcp":{}},"channels":[{"type":"inject"}]}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_013: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-013")
            .collect();
        assert_eq!(pl_013.len(), 1);
        assert!(
            pl_013[0].message.contains("missing"),
            "Should mention missing 'server' field"
        );
    }

    #[test]
    fn test_cc_pl_013_server_not_in_mcp_servers() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","mcpServers":{"myMcp":{}},"channels":[{"server":"nonexistent"}]}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_013: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-013")
            .collect();
        assert_eq!(pl_013.len(), 1);
        assert!(
            pl_013[0].message.contains("nonexistent"),
            "Should mention the unresolved server reference"
        );
    }

    #[test]
    fn test_cc_pl_013_valid_server_reference_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","mcpServers":{"myMcp":{}},"channels":[{"server":"myMcp"}]}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-013"));
    }

    #[test]
    fn test_cc_pl_013_no_channels_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-013"));
    }

    // ===== CC-PL-014: Plugin Agent Uses Unsupported Field =====

    #[test]
    fn test_cc_pl_014_agent_with_hooks() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","agents":{"myAgent":{"hooks":{"preCommit":"echo test"}}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_014: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-014")
            .collect();
        assert_eq!(pl_014.len(), 1);
        assert!(
            pl_014[0].message.contains("hooks"),
            "Should mention unsupported 'hooks' field"
        );
    }

    #[test]
    fn test_cc_pl_014_agent_with_multiple_unsupported() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","agents":{"myAgent":{"hooks":{},"mcpServers":{},"permissionMode":"full"}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        let pl_014: Vec<_> = diagnostics
            .iter()
            .filter(|d| d.rule == "CC-PL-014")
            .collect();
        assert_eq!(pl_014.len(), 3, "Should flag all three unsupported fields");
    }

    #[test]
    fn test_cc_pl_014_agent_without_unsupported_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","agents":{"myAgent":{"name":"Agent","description":"A test agent"}}}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-014"));
    }

    #[test]
    fn test_cc_pl_014_no_agents_no_error() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-014"));
    }

    // ===== CC-PL-015: Default Component Folder Shadowed by Manifest =====

    #[test]
    fn test_cc_pl_015_default_component_folder_shadowed() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":"./custom-commands"}"#,
        );
        fs::create_dir_all(temp.path().join("commands")).unwrap();

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(diagnostics.iter().any(|d| d.rule == "CC-PL-015"));
    }

    #[test]
    fn test_cc_pl_015_manifest_includes_default_component() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":["./custom-commands","./commands"]}"#,
        );
        fs::create_dir_all(temp.path().join("commands")).unwrap();

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-015"));
    }

    #[test]
    fn test_cc_pl_015_no_default_component_folder() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":"./custom-commands"}"#,
        );

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-015"));
    }

    #[test]
    fn test_cc_pl_015_ignores_default_component_file() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":"./custom-commands"}"#,
        );
        fs::write(temp.path().join("commands"), "").unwrap();

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-015"));
    }

    #[test]
    fn test_cc_pl_015_skips_invalid_manifest_path_shape() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":{}}"#,
        );
        fs::create_dir_all(temp.path().join("commands")).unwrap();

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &LintConfig::default(),
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-015"));
    }

    #[test]
    fn test_cc_pl_015_disabled() {
        let temp = TempDir::new().unwrap();
        let plugin_path = temp.path().join(".claude-plugin").join("plugin.json");
        write_plugin(
            &plugin_path,
            r#"{"name":"test","description":"desc","version":"1.0.0","commands":"./custom-commands"}"#,
        );
        fs::create_dir_all(temp.path().join("commands")).unwrap();

        let mut config = LintConfig::default();
        config.rules_mut().disabled_rules = vec!["CC-PL-015".to_string()];

        let validator = PluginValidator;
        let diagnostics = validator.validate(
            &plugin_path,
            &fs::read_to_string(&plugin_path).unwrap(),
            &config,
        );

        assert!(!diagnostics.iter().any(|d| d.rule == "CC-PL-015"));
    }
}
