// AST → IR lowering - converts parser output to engine IR

use super::{ConditionalBranch, Element, IRNode, Props, Value};
use crate::reactive::{extract_bindings_from_expression, parse_binding, Binding};
use hypen_parser::{ComponentSpecification, Value as ParserValue};
use std::collections::HashSet;

// Event handling is now done at the renderer level, not in the engine

/// Expand Tailwind classes into CSS properties
/// Returns a Vec of (prop_name, value) pairs
fn expand_tailwind_classes(classes: &str) -> Vec<(String, Value)> {
    let output = hypen_tailwind_parse::parse_classes(classes);
    let mut props = Vec::new();

    // Add base properties (no variant)
    for css_prop in &output.base {
        props.push((
            css_prop.property.clone(),
            Value::Static(serde_json::Value::String(css_prop.value.clone())),
        ));
    }

    // Add variant properties with suffix notation
    // e.g., "padding@md" for responsive, "backgroundColor:hover" for state
    for (variant_key, css_props) in &output.variants {
        for css_prop in css_props {
            let prop_key = format!("{}{}", css_prop.property, variant_key);
            props.push((
                prop_key,
                Value::Static(serde_json::Value::String(css_prop.value.clone())),
            ));
        }
    }

    props
}

/// Extract all ${state.xxx} and ${item.xxx} bindings from a template string
/// This handles both simple bindings like ${state.x} and expressions like ${state.x ? 'a' : 'b'}
/// Returns None if no bindings found, Some(bindings) if bindings exist
fn extract_bindings_from_template(s: &str) -> Option<Vec<Binding>> {
    let mut bindings = Vec::new();
    let mut seen_paths: HashSet<String> = HashSet::new();
    let mut pos = 0;

    // Look for ${ patterns and try to parse them
    while let Some(start) = s[pos..].find("${") {
        let abs_start = pos + start;
        if let Some(end) = s[abs_start..].find('}') {
            let abs_end = abs_start + end;
            // Extract the full binding string including ${...}
            let binding_str = &s[abs_start..=abs_end];

            // Try to parse as a simple binding first
            if let Some(binding) = parse_binding(binding_str) {
                let path = binding.full_path_with_source();
                if !seen_paths.contains(&path) {
                    seen_paths.insert(path);
                    bindings.push(binding);
                }
            } else {
                // Not a simple binding - extract bindings from the expression
                let expr_content = &s[abs_start + 2..abs_end];
                for binding in extract_bindings_from_expression(expr_content) {
                    let path = binding.full_path_with_source();
                    if !seen_paths.contains(&path) {
                        seen_paths.insert(path);
                        bindings.push(binding);
                    }
                }
            }
            pos = abs_end + 1;
        } else {
            break;
        }
    }

    if bindings.is_empty() {
        None
    } else {
        Some(bindings)
    }
}

/// Process applicators from a parsed component and insert them as props.
///
/// Handles:
/// - `.tw(classes)` - expands Tailwind classes to CSS properties
/// - `.bind(path)` - two-way binding for form elements (state only)
/// - All other applicators - converted to namespaced props
///
/// `element_type` is used by `.bind()` to pick the correct bound property
/// (`checked` for Checkbox, `on` for Switch, `value` otherwise).
fn process_applicators(
    applicators: &[hypen_parser::ApplicatorSpecification],
    props: &mut Props,
    element_type: &str,
) {
    for applicator in applicators {
        // .tw(classes) → expand Tailwind to individual CSS props
        if applicator.name == "tw" {
            if let Some(arg) = applicator.arguments.arguments.first() {
                let class_string = match arg {
                    hypen_parser::Argument::Named { value, .. }
                    | hypen_parser::Argument::Positioned { value, .. } => {
                        if let ParserValue::String(s) = value {
                            s.trim_matches(|c: char| c == '"' || c == '\'').to_string()
                        } else {
                            continue;
                        }
                    }
                };
                for (prop_key, prop_value) in expand_tailwind_classes(&class_string) {
                    props.insert(prop_key, prop_value);
                }
            }
            continue;
        }

        // .bind(@state.path) → two-way binding
        if applicator.name == "bind" {
            if let Some(arg) = applicator.arguments.arguments.first() {
                let value = match arg {
                    hypen_parser::Argument::Named { value, .. }
                    | hypen_parser::Argument::Positioned { value, .. } => value,
                };
                let ir_value = parser_value_to_ir(value);
                if let Value::Binding(binding) = ir_value {
                    if binding.is_state() {
                        let path = binding.full_path();
                        let prop_name = match element_type {
                            "Checkbox" | "checkbox" => "checked",
                            "Switch" | "switch" => "on",
                            _ => "value",
                        };
                        props.insert(prop_name.to_string(), Value::Binding(binding));
                        props.insert(
                            "bind".to_string(),
                            Value::Static(serde_json::Value::String(path)),
                        );
                    }
                }
            }
            continue;
        }

        // All other applicators become namespaced props
        if applicator.arguments.arguments.is_empty() {
            // Zero-argument applicators default to boolean true
            // e.g. .fillMaxWidth() → fillMaxWidth = true
            props.insert(
                format!("{}.0", applicator.name),
                Value::Static(serde_json::json!(true)),
            );
        } else {
            for (i, arg) in applicator.arguments.arguments.iter().enumerate() {
                let (key, value) = match arg {
                    hypen_parser::Argument::Named { key, value } => (
                        format!("{}.{}", applicator.name, key),
                        parser_value_to_ir(value),
                    ),
                    hypen_parser::Argument::Positioned { value, .. } => (
                        format!("{}.{}", applicator.name, i),
                        parser_value_to_ir(value),
                    ),
                };
                props.insert(key, value);
            }
        }
    }
}

/// Convert parser AST to engine IR
pub fn ast_to_ir(component: &ComponentSpecification) -> Element {
    let mut element = Element::new(&component.name);

    // Debug: Log the component structure
    // #[cfg(target_arch = "wasm32")]
    // web_sys::console::log_1(&format!("🔍 Parsing component: {} with {} applicators", component.name, component.applicators.len()).into());

    // Convert arguments to props
    for (i, arg) in component.arguments.arguments.iter().enumerate() {
        let (key, value) = match arg {
            hypen_parser::Argument::Named { key, value } => {
                (key.clone(), parser_value_to_ir(value))
            }
            hypen_parser::Argument::Positioned { value, .. } => {
                // Positional args are mapped to numbered keys
                (i.to_string(), parser_value_to_ir(value))
            }
        };

        // All arguments become props - event handling is done at renderer level

        element.props.insert(key, value);
    }

    // Convert applicators to props
    process_applicators(
        &component.applicators,
        &mut element.props,
        &element.element_type,
    );

    // Optional key from first positional string argument or component id
    if let Some(hypen_parser::Argument::Positioned {
        value: ParserValue::String(s),
        ..
    }) = component.arguments.arguments.first()
    {
        element.key = Some(s.clone());
    }

    // Convert children recursively
    for child in &component.children {
        element
            .children
            .push_back(std::sync::Arc::new(ast_to_ir(child)));
    }

    element
}

/// Convert parser AST to engine IRNode (first-class control flow constructs)
/// This detects ForEach, When, and If components and converts them to the appropriate IRNode variants
pub fn ast_to_ir_node(component: &ComponentSpecification) -> IRNode {
    match component.name.as_str() {
        "ForEach" => convert_foreach(component),
        "When" => convert_when(component),
        "If" => convert_if(component),
        _ => {
            // Regular element - convert children to IRNodes recursively
            let mut element = Element::new(&component.name);

            // Convert arguments to props
            for (i, arg) in component.arguments.arguments.iter().enumerate() {
                let (key, value) = match arg {
                    hypen_parser::Argument::Named { key, value } => {
                        (key.clone(), parser_value_to_ir(value))
                    }
                    hypen_parser::Argument::Positioned { value, .. } => {
                        (i.to_string(), parser_value_to_ir(value))
                    }
                };
                element.props.insert(key, value);
            }

            // Convert applicators to props
            process_applicators(
                &component.applicators,
                &mut element.props,
                &element.element_type,
            );

            // Optional key from first positional string argument
            if let Some(hypen_parser::Argument::Positioned {
                value: ParserValue::String(s),
                ..
            }) = component.arguments.arguments.first()
            {
                element.key = Some(s.clone());
            }

            // Convert children recursively as IRNodes
            // Check if this element should be treated as a list (has binding prop "0" and children)
            let has_binding_prop = element
                .props
                .get("0")
                .is_some_and(|v| matches!(v, Value::Binding(_)));
            if has_binding_prop && !component.children.is_empty() {
                // This is the legacy List pattern - convert to ForEach
                if let Some(Value::Binding(binding)) = element.props.get("0").cloned() {
                    let mut props = element.props.clone();
                    props.remove("0"); // Remove the array binding prop (uses COW)

                    let template: Vec<IRNode> =
                        component.children.iter().map(ast_to_ir_node).collect();

                    return IRNode::ForEach {
                        source: binding,
                        item_name: "item".to_string(),
                        key_path: None,
                        template,
                        props,
                    };
                }
            }

            // Regular element with children (Arc-wrapped for im::Vector<Arc<Element>>)
            element.children = component
                .children
                .iter()
                .map(|child| std::sync::Arc::new(ast_to_ir(child)))
                .collect();

            IRNode::Element(element)
        }
    }
}

/// Convert ForEach component to IRNode::ForEach
/// Syntax: ForEach(items: ${state.todos}, as: "todo", key: "id") { ... }
fn convert_foreach(component: &ComponentSpecification) -> IRNode {
    let mut source: Option<Binding> = None;
    let mut item_name = "item".to_string();
    let mut key_path: Option<String> = None;
    let mut props = Props::new();

    // Parse arguments
    for arg in &component.arguments.arguments {
        match arg {
            hypen_parser::Argument::Named { key, value } => {
                match key.as_str() {
                    "items" | "in" => {
                        // The source array binding
                        if let Value::Binding(binding) = parser_value_to_ir(value) {
                            source = Some(binding);
                        }
                    }
                    "as" => {
                        // The item variable name
                        if let ParserValue::String(s) = value {
                            item_name = s.trim_matches(|c: char| c == '"' || c == '\'').to_string();
                        }
                    }
                    "key" => {
                        // The key path for reconciliation
                        if let ParserValue::String(s) = value {
                            key_path = Some(s.trim_matches(|c: char| c == '"' || c == '\'').to_string());
                        }
                    }
                    _ => {
                        // Other props go to the container
                        props.insert(key.clone(), parser_value_to_ir(value));
                    }
                }
            }
            hypen_parser::Argument::Positioned { value, .. } => {
                // First positional arg is the source array
                if source.is_none() {
                    if let Value::Binding(binding) = parser_value_to_ir(value) {
                        source = Some(binding);
                    }
                }
            }
        }
    }

    // Convert applicators to props (ForEach uses "ForEach" as element_type for bind logic)
    process_applicators(&component.applicators, &mut props, "ForEach");

    // Convert children as template
    let template: Vec<IRNode> = component.children.iter().map(ast_to_ir_node).collect();

    let source = match source {
        Some(s) => s,
        None => {
            // Return an error element instead of panicking in WASM
            let mut err = Element::new("__Error");
            err.props.insert(
                "message".to_string(),
                Value::Static(serde_json::json!(
                    "ForEach requires an 'items' binding (e.g., ForEach(items: @state.list))"
                )),
            );
            return IRNode::Element(err);
        }
    };

    IRNode::ForEach {
        source,
        item_name,
        key_path,
        template,
        props,
    }
}

/// Convert When component to IRNode::Conditional
/// Syntax: When(value: ${state.status}) { Case(match: "loading") {...} Else {...} }
fn convert_when(component: &ComponentSpecification) -> IRNode {
    let mut condition_value: Option<Value> = None;

    // Parse the condition value
    for arg in &component.arguments.arguments {
        match arg {
            hypen_parser::Argument::Named { key, value } => {
                if key == "value" || key == "condition" {
                    condition_value = Some(parser_value_to_ir(value));
                }
            }
            hypen_parser::Argument::Positioned { value, .. } => {
                if condition_value.is_none() {
                    condition_value = Some(parser_value_to_ir(value));
                }
            }
        }
    }

    let mut branches = Vec::new();
    let mut fallback: Option<Vec<IRNode>> = None;

    // Parse children: Case and Else
    for child in &component.children {
        match child.name.as_str() {
            "Case" => {
                // Get the match pattern
                let pattern = child
                    .arguments
                    .arguments
                    .iter()
                    .find_map(|arg| match arg {
                        hypen_parser::Argument::Named { key, value } if key == "match" => {
                            Some(parser_value_to_ir(value))
                        }
                        hypen_parser::Argument::Positioned { value, .. } => {
                            Some(parser_value_to_ir(value))
                        }
                        _ => None,
                    })
                    .unwrap_or(Value::Static(serde_json::Value::Null));

                let children: Vec<IRNode> = child.children.iter().map(ast_to_ir_node).collect();

                branches.push(ConditionalBranch::new(pattern, children));
            }
            "Else" => {
                let children: Vec<IRNode> = child.children.iter().map(ast_to_ir_node).collect();
                fallback = Some(children);
            }
            _ => {
                // Non-Case/Else children become part of a default branch
                // This handles the case where When children are direct elements
            }
        }
    }

    let value = match condition_value {
        Some(v) => v,
        None => {
            let mut err = Element::new("__Error");
            err.props.insert(
                "message".to_string(),
                Value::Static(serde_json::json!(
                    "When requires a 'value' argument (e.g., When(value: @state.status))"
                )),
            );
            return IRNode::Element(err);
        }
    };

    IRNode::Conditional {
        value,
        branches,
        fallback,
    }
}

/// Convert If component to IRNode::Conditional (syntactic sugar over When with boolean matching)
/// Syntax: If(condition: ${state.loggedIn}) { ... Else { ... } }
fn convert_if(component: &ComponentSpecification) -> IRNode {
    let mut condition_value: Option<Value> = None;

    // Parse the condition
    for arg in &component.arguments.arguments {
        match arg {
            hypen_parser::Argument::Named { key, value } => {
                if key == "condition" || key == "when" {
                    condition_value = Some(parser_value_to_ir(value));
                }
            }
            hypen_parser::Argument::Positioned { value, .. } => {
                if condition_value.is_none() {
                    condition_value = Some(parser_value_to_ir(value));
                }
            }
        }
    }

    // Separate then-branch children from Else children
    let mut then_children: Vec<IRNode> = Vec::new();
    let mut else_children: Option<Vec<IRNode>> = None;

    for child in &component.children {
        if child.name == "Else" {
            else_children = Some(child.children.iter().map(ast_to_ir_node).collect());
        } else {
            then_children.push(ast_to_ir_node(child));
        }
    }

    // Create branches: true branch with then_children
    let branches = vec![ConditionalBranch::new(
        Value::Static(serde_json::json!(true)),
        then_children,
    )];

    let value = match condition_value {
        Some(v) => v,
        None => {
            let mut err = Element::new("__Error");
            err.props.insert(
                "message".to_string(),
                Value::Static(serde_json::json!(
                    "If requires a 'condition' argument (e.g., If(condition: @state.loggedIn))"
                )),
            );
            return IRNode::Element(err);
        }
    };

    IRNode::Conditional {
        value,
        branches,
        fallback: else_children,
    }
}

/// Convert parser Value to engine Value
fn parser_value_to_ir(value: &ParserValue) -> Value {
    match value {
        ParserValue::String(s) => {
            // Remove surrounding quotes if present
            let trimmed = s.trim_matches(|c: char| c == '"' || c == '\'');

            // Check if it's a pure binding (entire string is ${state.xxx} or ${item.xxx})
            if trimmed.starts_with("${")
                && trimmed.ends_with('}')
                && !trimmed[2..trimmed.len() - 1].contains("${")
            {
                // Try to parse as a simple binding first
                match parse_binding(trimmed) {
                    Some(binding) => Value::Binding(binding),
                    None => {
                        // Not a simple binding - might be an expression like ${state.active ? 'a' : 'b'}
                        // Extract bindings from the expression for dependency tracking
                        let bindings = extract_bindings_from_template(trimmed).unwrap_or_default();
                        Value::TemplateString {
                            template: trimmed.to_string(),
                            bindings,
                        }
                    }
                }
            } else if let Some(action_ref) = trimmed.strip_prefix('@') {
                // Strip @ prefix
                // Strip "actions." prefix if present
                let action_name = action_ref.strip_prefix("actions.").unwrap_or(action_ref);
                Value::Action(action_name.to_string())
            } else if trimmed.contains("${") {
                // Template string with embedded expressions or bindings
                // Extract any simple bindings we can find
                let bindings = extract_bindings_from_template(trimmed).unwrap_or_default();
                Value::TemplateString {
                    template: trimmed.to_string(),
                    bindings,
                }
            } else {
                Value::Static(serde_json::Value::String(trimmed.to_string()))
            }
        }
        ParserValue::Number(n) => Value::Static(serde_json::json!(*n)),
        ParserValue::Boolean(b) => Value::Static(serde_json::json!(*b)),
        ParserValue::List(items) => {
            let converted: Vec<serde_json::Value> = items
                .iter()
                .map(|v| match parser_value_to_ir(v) {
                    Value::Static(val) => val,
                    Value::Binding(binding) => {
                        serde_json::json!(format!("${{{}}}", binding.full_path()))
                    }
                    Value::TemplateString { template, .. } => serde_json::json!(template),
                    Value::Action(s) => serde_json::json!(format!("@{}", s)),
                })
                .collect();
            Value::Static(serde_json::json!(converted))
        }
        ParserValue::Map(map) => {
            let mut json_map = serde_json::Map::new();
            for (k, v) in map {
                // Handle all value types, not just Static values
                // This preserves bindings like ${item.id} inside maps
                match parser_value_to_ir(v) {
                    Value::Static(val) => {
                        json_map.insert(k.clone(), val);
                    }
                    Value::Binding(binding) => {
                        // Preserve binding as a string representation for later resolution
                        json_map.insert(
                            k.clone(),
                            serde_json::json!(format!("${{{}}}", binding.full_path_with_source())),
                        );
                    }
                    Value::TemplateString { template, .. } => {
                        json_map.insert(k.clone(), serde_json::json!(template));
                    }
                    Value::Action(s) => {
                        json_map.insert(k.clone(), serde_json::json!(format!("@actions.{}", s)));
                    }
                }
            }
            Value::Static(serde_json::Value::Object(json_map))
        }
        ParserValue::Reference(ref_str) => {
            // References like @state.user, @actions.login, @item, @item.name
            if ref_str.starts_with("state.") || ref_str.starts_with("item.") || ref_str == "item" {
                // Parse as binding, wrapping in ${}
                let binding_str = format!("${{{}}}", ref_str);
                match parse_binding(&binding_str) {
                    Some(binding) => Value::Binding(binding),
                    None => Value::Static(serde_json::Value::String(ref_str.clone())),
                }
            } else if let Some(action_name) = ref_str.strip_prefix("actions.") {
                Value::Action(action_name.to_string())
            } else {
                Value::Static(serde_json::Value::String(ref_str.clone()))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use hypen_parser::parse_component;

    #[test]
    fn test_simple_conversion() {
        let input = r#"Text("Hello World")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        assert_eq!(element.element_type, "Text");
        assert_eq!(element.props.len(), 1);
    }

    #[test]
    fn test_binding_conversion() {
        let input = r#"Text("${state.user.name}")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        // Check that binding was parsed correctly
        let first_prop = element.props.values().next().unwrap();
        match first_prop {
            Value::Binding(binding) => {
                assert_eq!(binding.path, vec!["user", "name"]);
                assert_eq!(binding.full_path(), "user.name");
            }
            _ => panic!("Expected binding, got: {:?}", first_prop),
        }
    }

    #[test]
    fn test_children_conversion() {
        let input = r#"
            Column {
                Text("First")
                Text("Second")
            }
        "#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        assert_eq!(element.element_type, "Column");
        assert_eq!(element.children.len(), 2);
        assert_eq!(element.children[0].element_type, "Text");
        assert_eq!(element.children[1].element_type, "Text");
    }

    #[test]
    fn test_onclick_applicator_conversion() {
        let input = r#"Text("Click me").onClick("@actions.increment")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        assert_eq!(element.element_type, "Text");
        // onClick should now be in props, not events
        assert!(element.props.contains_key("onClick.0"));
    }

    #[test]
    fn test_button_onclick_argument() {
        let input = r#"Button(onClick: "@actions.submit") { Text("Submit") }"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        assert_eq!(element.element_type, "Button");
        // onClick argument should be in props
        assert!(element.props.contains_key("onClick"));
        // Should have one child
        assert_eq!(element.children.len(), 1);
    }

    #[test]
    fn test_template_string_conversion() {
        let input = r#"Text("Count: ${state.count}")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        // Check that template string was parsed correctly
        let first_prop = element.props.values().next().unwrap();
        match first_prop {
            Value::TemplateString { template, bindings } => {
                assert_eq!(template, "Count: ${state.count}");
                assert_eq!(bindings.len(), 1);
                assert_eq!(bindings[0].full_path(), "count");
            }
            _ => panic!("Expected TemplateString, got: {:?}", first_prop),
        }
    }

    #[test]
    fn test_template_string_multiple_bindings() {
        let input = r#"Text("Hello ${state.user.name}, you have ${state.count} messages")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        let first_prop = element.props.values().next().unwrap();
        match first_prop {
            Value::TemplateString { template, bindings } => {
                assert_eq!(
                    template,
                    "Hello ${state.user.name}, you have ${state.count} messages"
                );
                assert_eq!(bindings.len(), 2);
                assert_eq!(bindings[0].full_path(), "user.name");
                assert_eq!(bindings[1].full_path(), "count");
            }
            _ => panic!("Expected TemplateString, got: {:?}", first_prop),
        }
    }

    #[test]
    fn test_static_string_no_bindings() {
        let input = r#"Text("Hello World")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        let first_prop = element.props.values().next().unwrap();
        match first_prop {
            Value::Static(val) => {
                assert_eq!(val.as_str().unwrap(), "Hello World");
            }
            _ => panic!("Expected Static, got: {:?}", first_prop),
        }
    }

    #[test]
    fn test_item_reference_conversion() {
        // Test @item.name reference syntax
        let input = r#"Text(text: @item.name)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        let text_prop = element.props.get("text").unwrap();
        match text_prop {
            Value::Binding(binding) => {
                assert!(binding.is_item(), "Should be an item binding");
                assert_eq!(binding.path, vec!["name"]);
                assert_eq!(binding.full_path(), "name");
                assert_eq!(binding.full_path_with_source(), "item.name");
            }
            _ => panic!("Expected item Binding, got: {:?}", text_prop),
        }
    }

    #[test]
    fn test_item_reference_nested_path() {
        // Test @item.user.profile.name reference syntax
        let input = r#"Text(text: @item.user.profile.name)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        let text_prop = element.props.get("text").unwrap();
        match text_prop {
            Value::Binding(binding) => {
                assert!(binding.is_item(), "Should be an item binding");
                assert_eq!(binding.path, vec!["user", "profile", "name"]);
                assert_eq!(binding.full_path(), "user.profile.name");
            }
            _ => panic!("Expected item Binding, got: {:?}", text_prop),
        }
    }

    #[test]
    fn test_bare_item_reference() {
        // Test @item reference (the whole item object)
        let input = r#"Component(data: @item)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        let data_prop = element.props.get("data").unwrap();
        match data_prop {
            Value::Binding(binding) => {
                assert!(binding.is_item(), "Should be an item binding");
                assert!(
                    binding.path.is_empty(),
                    "Path should be empty for bare @item"
                );
                assert_eq!(binding.full_path_with_source(), "item");
            }
            _ => panic!("Expected item Binding, got: {:?}", data_prop),
        }
    }

    #[test]
    fn test_item_binding_in_template_string() {
        // Test ${item.name} in a template string
        let input = r#"Text("Hello ${item.name}!")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        let first_prop = element.props.values().next().unwrap();
        match first_prop {
            Value::TemplateString { template, bindings } => {
                assert_eq!(template, "Hello ${item.name}!");
                assert_eq!(bindings.len(), 1);
                assert!(bindings[0].is_item(), "Should be an item binding");
                assert_eq!(bindings[0].full_path(), "name");
            }
            _ => panic!(
                "Expected TemplateString with item binding, got: {:?}",
                first_prop
            ),
        }
    }

    #[test]
    fn test_mixed_state_and_item_bindings() {
        // Test template with both ${state.xxx} and ${item.xxx}
        let input = r#"Text("${state.prefix}: ${item.name}")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        let first_prop = element.props.values().next().unwrap();
        match first_prop {
            Value::TemplateString { template, bindings } => {
                assert_eq!(template, "${state.prefix}: ${item.name}");
                assert_eq!(bindings.len(), 2);
                assert!(bindings[0].is_state(), "First should be state binding");
                assert_eq!(bindings[0].full_path(), "prefix");
                assert!(bindings[1].is_item(), "Second should be item binding");
                assert_eq!(bindings[1].full_path(), "name");
            }
            _ => panic!("Expected TemplateString, got: {:?}", first_prop),
        }
    }

    #[test]
    fn test_tw_applicator_expansion() {
        // Test that .tw() applicator expands to CSS properties
        let input = r#"Text("Hello").tw("p-4 text-blue-500")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        // Should have expanded padding and color
        assert!(
            element.props.contains_key("padding"),
            "Should have padding prop"
        );
        assert!(
            element.props.contains_key("color"),
            "Should have color prop"
        );

        // Check padding value
        if let Value::Static(val) = element.props.get("padding").unwrap() {
            assert_eq!(val.as_str().unwrap(), "1rem");
        } else {
            panic!("Expected static padding value");
        }

        // Check color value
        if let Value::Static(val) = element.props.get("color").unwrap() {
            assert_eq!(val.as_str().unwrap(), "#3b82f6");
        } else {
            panic!("Expected static color value");
        }
    }

    #[test]
    fn test_tw_applicator_with_variants() {
        // Test that .tw() applicator handles responsive variants
        let input = r#"Text("Hello").tw("p-4 md:p-8 hover:bg-white")"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        // Should have base padding
        assert!(
            element.props.contains_key("padding"),
            "Should have padding prop"
        );

        // Should have variant properties
        assert!(
            element.props.contains_key("padding@md"),
            "Should have padding@md prop"
        );
        assert!(
            element.props.contains_key("background-color:hover"),
            "Should have background-color:hover prop"
        );

        // Check values
        if let Value::Static(val) = element.props.get("padding@md").unwrap() {
            assert_eq!(val.as_str().unwrap(), "2rem");
        }
        if let Value::Static(val) = element.props.get("background-color:hover").unwrap() {
            assert_eq!(val.as_str().unwrap(), "#ffffff");
        }
    }

    #[test]
    fn test_tw_mixed_with_other_applicators() {
        // Test .tw() combined with other applicators
        let input = r#"Text("Hello").tw("p-4").fontSize(18)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        // Should have expanded tailwind
        assert!(element.props.contains_key("padding"));

        // Should also have regular applicator
        assert!(element.props.contains_key("fontSize.0"));
    }

    #[test]
    fn test_bind_applicator_expansion() {
        let input = r#"Input(placeholder: "Type...").bind(@state.message)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        // Should have "value" as a binding
        match element.props.get("value").unwrap() {
            Value::Binding(binding) => {
                assert!(binding.is_state());
                assert_eq!(binding.full_path(), "message");
            }
            _ => panic!("Expected binding for value prop"),
        }

        // Should have "bind" with the state path string
        match element.props.get("bind").unwrap() {
            Value::Static(val) => assert_eq!(val.as_str().unwrap(), "message"),
            _ => panic!("Expected static string for bind prop"),
        }
    }

    #[test]
    fn test_bind_applicator_nested_path() {
        let input = r#"Input(placeholder: "Email").bind(@state.user.email)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        match element.props.get("value").unwrap() {
            Value::Binding(binding) => {
                assert!(binding.is_state());
                assert_eq!(binding.full_path(), "user.email");
            }
            _ => panic!("Expected binding for value prop"),
        }

        match element.props.get("bind").unwrap() {
            Value::Static(val) => assert_eq!(val.as_str().unwrap(), "user.email"),
            _ => panic!("Expected static string for bind prop"),
        }
    }

    #[test]
    fn test_bind_applicator_item_binding_ignored() {
        // .bind(@item.x) should NOT produce bind props (can't write back to iteration items)
        let input = r#"Input(placeholder: "Name").bind(@item.name)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        assert!(
            !element.props.contains_key("bind"),
            "Item bindings should not produce bind prop"
        );
        assert!(
            !element.props.contains_key("value"),
            "Item bindings should not produce value prop"
        );
    }

    #[test]
    fn test_bind_applicator_in_ir_node() {
        // Test that bind works through ast_to_ir_node too
        let input = r#"Textarea(placeholder: "Message").bind(@state.msg)"#;
        let component = parse_component(input).unwrap();
        let ir_node = ast_to_ir_node(&component);

        match ir_node {
            IRNode::Element(element) => {
                match element.props.get("value").unwrap() {
                    Value::Binding(binding) => {
                        assert!(binding.is_state());
                        assert_eq!(binding.full_path(), "msg");
                    }
                    _ => panic!("Expected binding for value prop"),
                }
                match element.props.get("bind").unwrap() {
                    Value::Static(val) => assert_eq!(val.as_str().unwrap(), "msg"),
                    _ => panic!("Expected static string for bind prop"),
                }
            }
            _ => panic!("Expected Element IRNode"),
        }
    }

    #[test]
    fn test_bind_checkbox_uses_checked_prop() {
        let input = r#"Checkbox(label: "Accept").bind(@state.accepted)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        // Should have "checked" (not "value") as a binding
        assert!(
            !element.props.contains_key("value"),
            "Checkbox bind should not set value prop"
        );
        match element.props.get("checked").unwrap() {
            Value::Binding(binding) => {
                assert!(binding.is_state());
                assert_eq!(binding.full_path(), "accepted");
            }
            _ => panic!("Expected binding for checked prop"),
        }

        match element.props.get("bind").unwrap() {
            Value::Static(val) => assert_eq!(val.as_str().unwrap(), "accepted"),
            _ => panic!("Expected static string for bind prop"),
        }
    }

    #[test]
    fn test_bind_switch_uses_on_prop() {
        let input = r#"Switch(label: "Dark mode").bind(@state.darkMode)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        // Should have "on" (not "value") as a binding
        assert!(
            !element.props.contains_key("value"),
            "Switch bind should not set value prop"
        );
        match element.props.get("on").unwrap() {
            Value::Binding(binding) => {
                assert!(binding.is_state());
                assert_eq!(binding.full_path(), "darkMode");
            }
            _ => panic!("Expected binding for on prop"),
        }

        match element.props.get("bind").unwrap() {
            Value::Static(val) => assert_eq!(val.as_str().unwrap(), "darkMode"),
            _ => panic!("Expected static string for bind prop"),
        }
    }

    #[test]
    fn test_bind_select_uses_value_prop() {
        let input = r#"Select(options: ["a", "b"]).bind(@state.selected)"#;
        let component = parse_component(input).unwrap();
        let element = ast_to_ir(&component);

        // Select should use "value" prop
        match element.props.get("value").unwrap() {
            Value::Binding(binding) => {
                assert!(binding.is_state());
                assert_eq!(binding.full_path(), "selected");
            }
            _ => panic!("Expected binding for value prop"),
        }

        match element.props.get("bind").unwrap() {
            Value::Static(val) => assert_eq!(val.as_str().unwrap(), "selected"),
            _ => panic!("Expected static string for bind prop"),
        }
    }
}
