use crate::token_parser::Element;
use crate::token_parser::Node;
use crate::tag_data::{VALID_TAGS, GLOBAL_ATTRIBUTES, COMMON_ATTRIBUTES};
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};

/// Check if a normalized token string contains a reference to Azumi's `Raw` type.
///
/// Detects: `Raw(`, `Raw::<`, `::Raw(`, `r#Raw(`
/// Avoids false positives on identifiers ending in `Raw` (e.g., `get_Raw::value()`).
fn contains_raw_reference(normalized: &str) -> bool {
    normalized.contains("Raw(")
        || normalized.contains("Raw::<")
        || normalized.contains("::Raw(")
        || normalized.contains("r#Raw(")
}

/// Validate Raw usage patterns - ERROR on ANY Raw() in html! expressions.
///
/// Raw() bypasses ALL of Azumi's safety guarantees. Use the safe alternatives:
/// - `json_data!("var" = &data)` for JSON data injection
/// - `<style>{var}</style>` for CSS content (auto-escaped inside html!)
/// - `<script>{var}</script>` for JavaScript content (auto-escaped inside html!)
/// - Standard `{value}` interpolation for text (auto-escaped)
pub fn validate_raw_usage(nodes: &[Node]) -> Vec<TokenStream> {
    let mut errors = vec![];

    fn check_node(node: &Node, errors: &mut Vec<TokenStream>) {
        match node {
            Node::Expression(expr) => {
                let content_str = expr.content.to_string();
                let normalized_str = content_str.replace(' ', "");
                if contains_raw_reference(&normalized_str) {
                    errors.push(quote_spanned! { expr.span =>
                            compile_error!(
                                "Azumi: Raw() is not allowed inside html!.\n\n\
                                Raw() bypasses ALL of Azumi's safety guarantees — escaping, CSS scoping, \n\
                                compile-time validation, and security checks.\n\
                                \n\
                                ✅ Use Azumi's safe alternatives:\n\
                                \n\
                                // For injected text content — use {value} interpolation:\n\
                                html! { <p>{value}</p> }\n\
                                \n\
                                // For JSON data to JavaScript — use json_data! macro:\n\
                                html! { {azumi::json_data!(\"MY_DATA\" = &data)} }\n\
                                \n\
                                // For CSS content — use <style> tag with {variable}:\n\
                                html! { <style>{GLOBAL_CSS}</style> }\n\
                                \n\
                                // For JavaScript content — use <script> tag with {variable}:\n\
                                html! { <script>{TRACKING_JS}</script> }\n\
                                \n\
                                ❌ Wrong — Raw() bypasses all safety:\n\
                                html! { @{Raw(\"...\")} }\n\
                                html! { @{Raw(format!(\"...\"))} }\n\
                                \n\
                                See: docs/guide.md"
                            );
                    });
                }
            }
            Node::Element(elem) => {
                for child in &elem.children {
                    check_node(child, errors);
                }
                // Rule: Raw() is also banned inside attribute values
                for attr in &elem.attrs {
                    match &attr.value {
                        crate::token_parser::AttributeValue::Dynamic(tokens) => {
                            let normalized = tokens.to_string().replace(' ', "");
                            if contains_raw_reference(&normalized) {
                                let attr_name = &attr.name;
                                errors.push(quote! {
                                    compile_error!(concat!(
                                        "Azumi: `@{{Raw(...)}}` is not allowed inside `html!`.\n",
                                        "It bypasses all escaping and is a major XSS risk.\n",
                                        "Found in attribute `", #attr_name, "`.\n",
                                        "Use safe interpolation `{{value}}` or auto-escaping tags instead."
                                    ));
                                });
                            }
                        }
                        crate::token_parser::AttributeValue::StyleDsl(pairs) => {
                            for (_, value_tokens) in pairs {
                                let normalized = value_tokens.to_string().replace(' ', "");
                                if contains_raw_reference(&normalized) {
                                    let attr_name = &attr.name;
                                    errors.push(quote! {
                                        compile_error!(concat!(
                                            "Azumi: `@{{Raw(...)}}` is not allowed inside `html!`.\n",
                                            "It bypasses all escaping and is a major XSS risk.\n",
                                            "Found in style DSL of attribute `", #attr_name, "`.\n",
                                            "Use safe interpolation `{{value}}` or auto-escaping tags instead."
                                        ));
                                    });
                                }
                            }
                        }
                        _ => {}
                    }
                }
            }
            Node::Fragment(frag) => {
                for child in &frag.children {
                    check_node(child, errors);
                }
            }
            Node::Block(block) => {
                match block {
                    crate::token_parser::Block::If(if_block) => {
                        for child in &if_block.then_branch {
                            check_node(child, errors);
                        }
                        if let Some(else_branch) = &if_block.else_branch {
                            for child in else_branch {
                                check_node(child, errors);
                            }
                        }
                    }
                    crate::token_parser::Block::For(for_block) => {
                        for child in &for_block.body {
                            check_node(child, errors);
                        }
                    }
                    crate::token_parser::Block::Match(match_block) => {
                        for arm in &match_block.arms {
                            for child in &arm.body {
                                check_node(child, errors);
                            }
                        }
                    }
                    crate::token_parser::Block::Call(call_block) => {
                        for child in &call_block.children {
                            check_node(child, errors);
                        }
                    }
                    crate::token_parser::Block::Component(_) => {
                        // Component reference has no child nodes to validate
                    }
                    crate::token_parser::Block::Let(_) => {
                        // Let binding has no child nodes to validate
                    }
                    crate::token_parser::Block::Style(_) => {
                        // Style block content is CSS, not nodes
                    }
                }
            }
            _ => {}
        }
    }

    for node in nodes {
        check_node(node, &mut errors);
    }

    errors
}

/// Rule 10: Component Structure Enforcement
/// Enforces Script -> Content -> Style order
pub fn validate_node_order(nodes: &[Node]) -> Vec<TokenStream> {
    let mut errors = vec![];

    #[derive(PartialEq, PartialOrd, Copy, Clone)]
    enum Phase {
        Script,
        Content,
        Style,
    }

    let mut phase = Phase::Script;

    for node in nodes {
        match node {
            // Script Handling
            Node::Element(elem) if elem.name == "script" => {
                if phase > Phase::Script {
                    let msg = if phase == Phase::Style {
                        "Order Error: <script> tags must be placed before <style> blocks and HTML content. Move this <script> above the <style> block."
                    } else {
                        "Order Error: <script> tags must be placed at the top of the component, before any HTML content."
                    };
                    errors.push(quote_spanned! { elem.full_span =>
                        compile_error!(#msg);
                    });
                }
                // Allowed in Script Phase, stays in Script Phase
            }
            // Style Handling
            Node::Element(elem) if elem.name == "style" => {
                // <style> element (e.g., inline or src) - treat as Style phase
                phase = Phase::Style;
            }
            Node::Block(crate::token_parser::Block::Style(_)) => {
                // style! block - treat as Style phase
                phase = Phase::Style;
            }
            // Comments - Ignored, do not change phase
            Node::Comment(_) => {}
            // All other content (HTML elements, text, blocks, etc.)
            _ => {
                if phase == Phase::Style {
                    // Start of content span is tricky without a specific node match,
                    // but we can match individual types if needed or just use a generic span if available.
                    // For now, let's try to get a span from the node if possible
                    let span = match node {
                        Node::Element(e) => e.full_span,
                        Node::Text(t) => t.span,
                        Node::Expression(e) => e.span,
                        Node::Doctype(d) => d.span,
                        Node::Fragment(f) => f.span,
                        Node::Block(b) => match b {
                            crate::token_parser::Block::If(i) => i.span,
                            crate::token_parser::Block::For(f) => f.span,
                            crate::token_parser::Block::Match(m) => m.span,
                            crate::token_parser::Block::Call(c) => c.span,
                            crate::token_parser::Block::Component(c) => c.span,
                            crate::token_parser::Block::Let(l) => l.span,
                            _ => proc_macro2::Span::call_site(), // Should match matches above
                        },
                        _ => proc_macro2::Span::call_site(),
                    };

                    let msg = "Order Error: HTML structure and logic must be placed BEFORE Style blocks. Move this content above the <style> block.";
                    errors.push(quote_spanned! { span =>
                        compile_error!(#msg);
                    });
                } else {
                    // Move to Content phase if we were in Script phase
                    if phase == Phase::Script {
                        phase = Phase::Content;
                    }
                }
            }
        }
    }

    errors
}

// HTML structure rules — delegated to html_rules module
pub use crate::html_rules::{
    validate_table_children,
    validate_list_children,
    validate_nested_forms,
    validate_button_interactive,
    validate_paragraph_content,
    validate_anchor_nesting,
    validate_heading_content,
};
/// Rule 8: Tag names must be valid HTML5 tags or custom elements (with dashes)
pub fn validate_tag_name(elem: &Element) -> Option<TokenStream> {
    let name = &elem.name;

    // Allow custom elements (must contain a dash)
    if name.contains('-') {
        return None;
    }

    // Allow standard HTML5 tags
    // Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Element
    if !VALID_TAGS.contains(&name.as_str()) {
        let msg = format!(
            "Unknown tag <{}>. If this is a custom element, it must contain a dash (e.g., <my-component>). If it's a standard HTML tag, check for typos.",
            name
        );
        return Some(quote_spanned! { elem.span =>
            compile_error!(#msg);
        });
    }

    None
}

/// Rule 9: Attribute names must be valid HTML attributes, data-*, aria-*, or event handlers
pub fn validate_attribute_name(attr: &crate::token_parser::Attribute) -> Option<TokenStream> {
    let name = &attr.name;

    // 1. Allow common hyphenated attribute prefixes
    // This covers data-*, aria-*, hx-*, x-*, and standard custom attributes.
    // Reject obvious non-standard attributes like onclick-foo.
    if name.contains('-') {
        // Ensure it starts with a known prefix pattern
        let is_known_prefix = name.starts_with("data-")
            || name.starts_with("aria-")
            || name.starts_with("hx-")
            || name.starts_with("x-")
            || name.starts_with("az-")
            || name.starts_with("itemid")
            || name.starts_with("itemprop")
            || name.starts_with("itemref")
            || name.starts_with("itemscope")
            || name.starts_with("itemtype")
            || name.starts_with("accept-")
            || name.starts_with("crossorigin")
            || name.starts_with("http-equiv")
            || name.starts_with("stroke-")
            || name.starts_with("fill-")
            || name.starts_with("clip-")
            || name.starts_with("gradient-");
        if is_known_prefix {
            return None;
        }
        // Allow any attribute where the part before the first hyphen is a valid HTML attribute name
        // This catches things like 'popovertarget' (not hyphenated but valid)
        // For hyphenated, be more restrictive
    }

    // 2. Check for Azumi event DSL (on:event) vs native HTML events (onevent)
    if name.starts_with("on:") {
        // This is Azumi's event DSL like on:click, on:mouseover - allowed
        return None;
    }
    if let Some(stripped) = name.strip_prefix("on") {
        // Native HTML event handler like onclick, onmouseover
        // Suggest using Azumi's on:event syntax instead
        let suggestion = format!("on:{}", stripped.to_lowercase());
        let msg = format!(
            "Native event handler '{}' found. Did you mean to use Azumi's event DSL '{}'?\n\n\
             Azumi uses a different syntax for events:\n\
             ❌ onclick={{handle_click()}}\n\
             ✅ on:click={{handle_click}}\n\n\
             Note: In Azumi, 'on:click' means 'call handle_click when clicked'.",
            name, suggestion
        );
        return Some(quote_spanned! { attr.span =>
            compile_error!(#msg);
        });
    }

    // 3. Allow XML namespaces (xmlns) - though usually contains colon or is just xmlns
    if name == "xmlns" || name.contains(':') {
        return None;
    }

    // 4. Standard HTML Global Attributes
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
    if GLOBAL_ATTRIBUTES.contains(&name.as_str()) {
        return None;
    }

    // 5. Specific Element Attributes (Common ones)
    if COMMON_ATTRIBUTES.contains(&name.as_str()) {
        return None;
    }

    // If we get here, it's an unknown attribute
    let msg = format!(
        "Unknown attribute '{}'. Check for typos. If this is a custom attribute, prefix it with 'data-' (e.g., data-{}).",
        name, name
    );

    Some(quote_spanned! { attr.span =>
        compile_error!(#msg);
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::token_parser::Expression;
    use proc_macro2::TokenStream;

    fn create_expression_node(content: &str) -> Node {
        let tokens: TokenStream = content.parse().unwrap();
        Node::Expression(Expression {
            content: tokens,
            span: proc_macro2::Span::call_site(),
        })
    }

    #[test]
    fn test_css_in_raw_detected_style_tag() {
        let node = create_expression_node(r#"Raw("<style>.foo { color: red; }</style>")"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "Should detect CSS in Raw with <style> tag"
        );
        let error_str = errors[0].to_string();
        assert!(
            error_str.contains("Raw() is not allowed"),
            "Error should mention Raw is not allowed, got: {}",
            error_str
        );
    }

    #[test]
    fn test_css_in_raw_detected_dot_class_syntax() {
        let node = create_expression_node(r#"Raw(".main { color: blue; }")"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "Should detect CSS in Raw with .class syntax"
        );
        let error_str = errors[0].to_string();
        assert!(
            error_str.contains("Raw() is not allowed"),
            "Error should mention Raw is not allowed, got: {}",
            error_str
        );
    }

    #[test]
    fn test_css_in_raw_detected_property_syntax() {
        let node = create_expression_node(r#"Raw(".container { padding: 1rem; }")"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "Should detect CSS in Raw with property syntax"
        );
    }

    #[test]
    fn test_css_in_raw_detected_color_property() {
        let node = create_expression_node(r#"Raw("{ color: #fff; }")"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "Should detect CSS color property in Raw"
        );
    }

    #[test]
    fn test_azumi_script_in_raw_now_blocked() {
        let node = create_expression_node(r#"Raw(azumi_script())"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "Raw(azumi_script()) should now be blocked - use {{azumi_script()}} instead"
        );
        let error_str = errors[0].to_string();
        assert!(
            error_str.contains("Raw() is not allowed"),
            "Should mention Raw is not allowed, got: {}",
            error_str
        )
    }

    #[test]
    fn test_window_location_in_raw_now_blocked() {
        let node = create_expression_node(r#"Raw("window.location.hash")"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "Raw with window.location should now be blocked"
        );
        let error_str = errors[0].to_string();
        assert!(
            error_str.contains("Raw() is not allowed"),
            "Should mention Raw is not allowed, got: {}",
            error_str
        );
    }

    #[test]
    fn test_suspicious_format_not_allowed() {
        let node = create_expression_node(r#"Raw(format!("<div>{}</div>", user_input))"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "format! with user input should be blocked"
        );
    }

    #[test]
    fn test_format_in_raw_always_blocked() {
        let node = create_expression_node(r#"Raw(format!("<div>{}</div>", some_var))"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "format! inside Raw() should always be blocked"
        );
        let error_str = errors[0].to_string();
        assert!(
            error_str.contains("Raw() is not allowed"),
            "Error should mention Raw is not allowed, got: {}",
            error_str
        );
    }

    #[test]
    fn test_js_in_raw_detected() {
        let node = create_expression_node(r#"Raw("<script>alert('hi')</script>")"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "JS inside Raw should be blocked"
        );
        let error_str = errors[0].to_string();
        assert!(
            error_str.contains("Raw() is not allowed"),
            "Error should mention Raw is not allowed, got: {}",
            error_str
        );
    }

    #[test]
    fn test_js_event_listener_in_raw_blocked() {
        let node = create_expression_node(r#"Raw("element.addEventListener('click', fn)")"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            !errors.is_empty(),
            "addEventListener inside Raw should be blocked"
        );
    }

    #[test]
    fn test_safe_expression_no_error() {
        let node = create_expression_node(r#"some_variable"#);
        let errors = validate_raw_usage(&[node]);

        assert!(
            errors.is_empty(),
            "Non-Raw expression should have no errors, got: {:?}",
            errors
        );
    }

    #[test]
    fn test_raw_colon_colon_turbofish_detected() {
        let node = create_expression_node(r#"Raw::<str>("hello")"#);
        let errors = validate_raw_usage(&[node]);
        assert!(
            !errors.is_empty(),
            "Raw::<str> turbofish should be detected"
        );
    }

    #[test]
    fn test_raw_qualified_path_detected() {
        let node = create_expression_node(r#"azumi::Raw("hello")"#);
        let errors = validate_raw_usage(&[node]);
        assert!(
            !errors.is_empty(),
            "azumi::Raw(...) qualified path should be detected"
        );
    }

    #[test]
    fn test_r_raw_raw_ident_detected() {
        let node = create_expression_node(r#"r#Raw("hello")"#);
        let errors = validate_raw_usage(&[node]);
        assert!(
            !errors.is_empty(),
            "r#Raw(...) raw identifier should be detected"
        );
    }

    #[test]
    fn test_draw_raw_renderer_no_false_positive() {
        let node = create_expression_node(r#"draw::RawRenderer::new()"#);
        let errors = validate_raw_usage(&[node]);
        assert!(
            errors.is_empty(),
            "draw::RawRenderer should NOT be flagged as Raw(), got: {:?}",
            errors
        );
    }

    #[test]
    fn test_raw_data_no_false_positive() {
        let node = create_expression_node(r#"raw_data.to_string()"#);
        let errors = validate_raw_usage(&[node]);
        assert!(
            errors.is_empty(),
            "raw_data variable should NOT be flagged, got: {:?}",
            errors
        );
    }

    #[test]
    fn test_get_raw_value_no_false_positive() {
        let node = create_expression_node(r#"get_raw_value()"#);
        let errors = validate_raw_usage(&[node]);
        assert!(
            errors.is_empty(),
            "get_raw_value() should NOT be flagged, got: {:?}",
            errors
        );
    }

    #[test]
    fn test_word_boundary_raw_colon_colon_no_false_positive() {
        let node = create_expression_node(r#"get_Raw::value()"#);
        let errors = validate_raw_usage(&[node]);
        assert!(
            errors.is_empty(),
            "get_Raw::value() should NOT be flagged (not the Raw type), got: {:?}",
            errors
        );
    }

    // =========================================================================
    // Structural Validator Tests
    // =========================================================================

    // Helper to create a simple element node
    fn element_node(name: &str) -> Node {
        Node::Element(Element {
            name: name.to_string(),
            attrs: vec![],
            children: vec![],
            bind_struct: None,
            span: proc_macro2::Span::call_site(),
            full_span: proc_macro2::Span::call_site(),
        })
    }

    fn element_with_children(name: &str, children: Vec<Node>) -> Node {
        Node::Element(Element {
            name: name.to_string(),
            attrs: vec![],
            children,
            bind_struct: None,
            span: proc_macro2::Span::call_site(),
            full_span: proc_macro2::Span::call_site(),
        })
    }

    // validate_node_order
    #[test]
    fn test_valid_order_content_then_style() {
        let nodes = vec![
            element_node("div"),
            Node::Block(crate::token_parser::Block::Style(crate::token_parser::StyleBlock { content: proc_macro2::TokenStream::new(), is_global: false, span: proc_macro2::Span::call_site() })),
        ];
        let errors = validate_node_order(&nodes);
        assert!(errors.is_empty(), "Content before style should be valid");
    }

    #[test]
    fn test_invalid_order_style_then_content() {
        let nodes = vec![
            Node::Block(crate::token_parser::Block::Style(crate::token_parser::StyleBlock { content: proc_macro2::TokenStream::new(), is_global: false, span: proc_macro2::Span::call_site() })),
            element_node("div"),
        ];
        let errors = validate_node_order(&nodes);
        assert!(!errors.is_empty(), "Content after style should be invalid");
    }

    #[test]
    fn test_valid_order_script_then_content_then_style() {
        let nodes = vec![
            element_node("script"),
            element_node("div"),
            Node::Block(crate::token_parser::Block::Style(crate::token_parser::StyleBlock { content: proc_macro2::TokenStream::new(), is_global: false, span: proc_macro2::Span::call_site() })),
        ];
        let errors = validate_node_order(&nodes);
        assert!(errors.is_empty(), "Script -> Content -> Style should be valid");
    }

    #[test]
    fn test_invalid_order_script_after_content() {
        let nodes = vec![
            element_node("div"),
            element_node("script"),
        ];
        let errors = validate_node_order(&nodes);
        assert!(!errors.is_empty(), "Script after content should be invalid");
    }

    // validate_table_children
    #[test]
    fn test_table_with_valid_children() {
        let table = element_with_children("table", vec![
            element_node("tr"),
            element_node("thead"),
            element_node("tbody"),
        ]);
        if let Node::Element(ref elem) = table {
            let errors = validate_table_children(elem);
            assert!(errors.is_empty(), "table with valid children should pass");
        }
    }

    #[test]
    fn test_table_with_invalid_child() {
        let table = element_with_children("table", vec![
            element_node("div"),
        ]);
        if let Node::Element(ref elem) = table {
            let errors = validate_table_children(elem);
            assert!(!errors.is_empty(), "div inside table should fail");
        }
    }

    #[test]
    fn test_non_table_ignored() {
        let div = element_with_children("div", vec![element_node("span")]);
        if let Node::Element(ref elem) = div {
            let errors = validate_table_children(elem);
            assert!(errors.is_empty(), "non-table should be ignored");
        }
    }

    // validate_list_children
    #[test]
    fn test_ul_with_li_children() {
        let ul = element_with_children("ul", vec![
            element_node("li"),
            element_node("li"),
        ]);
        if let Node::Element(ref elem) = ul {
            let errors = validate_list_children(elem);
            assert!(errors.is_empty(), "ul with li children should pass");
        }
    }

    #[test]
    fn test_ul_with_invalid_child() {
        let ul = element_with_children("ul", vec![
            element_node("div"),
        ]);
        if let Node::Element(ref elem) = ul {
            let errors = validate_list_children(elem);
            assert!(!errors.is_empty(), "div inside ul should fail");
        }
    }

    #[test]
    fn test_non_list_ignored() {
        let div = element_with_children("div", vec![element_node("span")]);
        if let Node::Element(ref elem) = div {
            let errors = validate_list_children(elem);
            assert!(errors.is_empty(), "non-list should be ignored");
        }
    }

    // validate_nested_forms
    #[test]
    fn test_form_inside_form_fails() {
        let form = element_node("form");
        if let Node::Element(ref elem) = form {
            let errors = validate_nested_forms(elem, true);
            assert!(!errors.is_empty(), "nested form should fail");
        }
    }

    #[test]
    fn test_form_not_inside_form_passes() {
        let form = element_node("form");
        if let Node::Element(ref elem) = form {
            let errors = validate_nested_forms(elem, false);
            assert!(errors.is_empty(), "form not nested should pass");
        }
    }

    #[test]
    fn test_non_form_ignored() {
        let div = element_node("div");
        if let Node::Element(ref elem) = div {
            let errors = validate_nested_forms(elem, true);
            assert!(errors.is_empty(), "non-form should be ignored even inside form");
        }
    }

    // validate_button_interactive
    #[test]
    fn test_button_inside_button_fails() {
        let button = element_node("button");
        if let Node::Element(ref elem) = button {
            let errors = validate_button_interactive(elem, true);
            assert!(!errors.is_empty(), "button inside button should fail");
        }
    }

    #[test]
    fn test_input_inside_button_fails() {
        let input = element_node("input");
        if let Node::Element(ref elem) = input {
            let errors = validate_button_interactive(elem, true);
            assert!(!errors.is_empty(), "input inside button should fail");
        }
    }

    #[test]
    fn test_span_inside_button_passes() {
        let span = element_node("span");
        if let Node::Element(ref elem) = span {
            let errors = validate_button_interactive(elem, true);
            assert!(errors.is_empty(), "span inside button should pass");
        }
    }

    #[test]
    fn test_not_inside_button_passes() {
        let button = element_node("button");
        if let Node::Element(ref elem) = button {
            let errors = validate_button_interactive(elem, false);
            assert!(errors.is_empty(), "not inside button should pass");
        }
    }

    // validate_paragraph_content
    #[test]
    fn test_div_inside_p_fails() {
        let p = element_with_children("p", vec![element_node("div")]);
        if let Node::Element(ref elem) = p {
            let errors = validate_paragraph_content(elem);
            assert!(!errors.is_empty(), "div inside p should fail");
        }
    }

    #[test]
    fn test_span_inside_p_passes() {
        let p = element_with_children("p", vec![element_node("span")]);
        if let Node::Element(ref elem) = p {
            let errors = validate_paragraph_content(elem);
            assert!(errors.is_empty(), "span inside p should pass");
        }
    }

    #[test]
    fn test_non_p_ignored() {
        let div = element_with_children("div", vec![element_node("div")]);
        if let Node::Element(ref elem) = div {
            let errors = validate_paragraph_content(elem);
            assert!(errors.is_empty(), "non-p should be ignored");
        }
    }

    // validate_anchor_nesting
    #[test]
    fn test_anchor_inside_anchor_fails() {
        let a = element_node("a");
        if let Node::Element(ref elem) = a {
            let errors = validate_anchor_nesting(elem, true);
            assert!(!errors.is_empty(), "nested anchor should fail");
        }
    }

    #[test]
    fn test_anchor_not_nested_passes() {
        let a = element_node("a");
        if let Node::Element(ref elem) = a {
            let errors = validate_anchor_nesting(elem, false);
            assert!(errors.is_empty(), "anchor not nested should pass");
        }
    }

    // validate_heading_content
    #[test]
    fn test_div_inside_h1_fails() {
        let h1 = element_with_children("h1", vec![element_node("div")]);
        if let Node::Element(ref elem) = h1 {
            let errors = validate_heading_content(elem);
            assert!(!errors.is_empty(), "div inside h1 should fail");
        }
    }

    #[test]
    fn test_span_inside_h1_passes() {
        let h1 = element_with_children("h1", vec![element_node("span")]);
        if let Node::Element(ref elem) = h1 {
            let errors = validate_heading_content(elem);
            assert!(errors.is_empty(), "span inside h1 should pass");
        }
    }

    #[test]
    fn test_non_heading_ignored() {
        let div = element_with_children("div", vec![element_node("div")]);
        if let Node::Element(ref elem) = div {
            let errors = validate_heading_content(elem);
            assert!(errors.is_empty(), "non-heading should be ignored");
        }
    }

    // validate_tag_name
    #[test]
    fn test_valid_tag_div() {
        let div = element_node("div");
        if let Node::Element(ref elem) = div {
            let result = validate_tag_name(elem);
            assert!(result.is_none(), "div should be a valid tag");
        }
    }

    #[test]
    fn test_custom_element_with_dash_passes() {
        let custom = element_node("my-component");
        if let Node::Element(ref elem) = custom {
            let result = validate_tag_name(elem);
            assert!(result.is_none(), "custom element with dash should pass");
        }
    }

    #[test]
    fn test_invalid_tag_fails() {
        let unknown = element_node("notatag");
        if let Node::Element(ref elem) = unknown {
            let result = validate_tag_name(elem);
            assert!(result.is_some(), "unknown tag should fail");
        }
    }

    // validate_attribute_name
    #[test]
    fn test_valid_attr_class() {
        let attr = crate::token_parser::Attribute {
            name: "class".to_string(),
            name_span: proc_macro2::Span::call_site(),
            value: crate::token_parser::AttributeValue::Static("foo".to_string()),
            span: proc_macro2::Span::call_site(),
            value_span: None,
        };
        let result = validate_attribute_name(&attr);
        assert!(result.is_none(), "class should be a valid attribute");
    }

    #[test]
    fn test_data_attr_passes() {
        let attr = crate::token_parser::Attribute {
            name: "data-value".to_string(),
            name_span: proc_macro2::Span::call_site(),
            value: crate::token_parser::AttributeValue::Static("123".to_string()),
            span: proc_macro2::Span::call_site(),
            value_span: None,
        };
        let result = validate_attribute_name(&attr);
        assert!(result.is_none(), "data-* should be a valid attribute");
    }

    #[test]
    fn test_on_event_passes() {
        let attr = crate::token_parser::Attribute {
            name: "on:click".to_string(),
            name_span: proc_macro2::Span::call_site(),
            value: crate::token_parser::AttributeValue::Static("handler".to_string()),
            span: proc_macro2::Span::call_site(),
            value_span: None,
        };
        let result = validate_attribute_name(&attr);
        assert!(result.is_none(), "on:event should be a valid attribute");
    }

    #[test]
    fn test_native_onclick_fails() {
        let attr = crate::token_parser::Attribute {
            name: "onclick".to_string(),
            name_span: proc_macro2::Span::call_site(),
            value: crate::token_parser::AttributeValue::Static("handler".to_string()),
            span: proc_macro2::Span::call_site(),
            value_span: None,
        };
        let result = validate_attribute_name(&attr);
        assert!(result.is_some(), "native onclick should fail with suggestion");
    }

    #[test]
    fn test_unknown_attr_fails() {
        let attr = crate::token_parser::Attribute {
            name: "notanattr".to_string(),
            name_span: proc_macro2::Span::call_site(),
            value: crate::token_parser::AttributeValue::Static("value".to_string()),
            span: proc_macro2::Span::call_site(),
            value_span: None,
        };
        let result = validate_attribute_name(&attr);
        assert!(result.is_some(), "unknown attribute should fail");
    }
}
