use std::sync::LazyLock;

use rustc_hash::FxHashSet;

use crate::template_usage::{TemplateSnippetKind, TemplateUsage, analyze_template_snippet};

use super::scanners::scan_curly_section;

/// Regex for stripping HTML comments (`<!-- ... -->`), shared by Vue and Svelte.
pub(super) static HTML_COMMENT_RE: LazyLock<regex::Regex> =
    LazyLock::new(|| regex::Regex::new(r"(?s)<!--.*?-->").expect("valid regex"));

pub(super) fn merge_expression_usage(
    usage: &mut TemplateUsage,
    snippet: &str,
    imported_bindings: &FxHashSet<String>,
    locals: &[String],
) {
    merge_snippet_usage(
        usage,
        snippet,
        TemplateSnippetKind::Expression,
        imported_bindings,
        locals,
        false,
    );
}

pub(super) fn merge_statement_usage(
    usage: &mut TemplateUsage,
    snippet: &str,
    imported_bindings: &FxHashSet<String>,
    locals: &[String],
) {
    merge_snippet_usage(
        usage,
        snippet,
        TemplateSnippetKind::Statement,
        imported_bindings,
        locals,
        false,
    );
}

pub(super) fn merge_expression_usage_allow_dollar_refs(
    usage: &mut TemplateUsage,
    snippet: &str,
    imported_bindings: &FxHashSet<String>,
    locals: &[String],
) {
    merge_snippet_usage(
        usage,
        snippet,
        TemplateSnippetKind::Expression,
        imported_bindings,
        locals,
        true,
    );
}

pub(super) fn merge_statement_usage_allow_dollar_refs(
    usage: &mut TemplateUsage,
    snippet: &str,
    imported_bindings: &FxHashSet<String>,
    locals: &[String],
) {
    merge_snippet_usage(
        usage,
        snippet,
        TemplateSnippetKind::Statement,
        imported_bindings,
        locals,
        true,
    );
}

fn merge_snippet_usage(
    usage: &mut TemplateUsage,
    snippet: &str,
    kind: TemplateSnippetKind,
    imported_bindings: &FxHashSet<String>,
    locals: &[String],
    allow_dollar_prefixed_refs: bool,
) {
    usage.merge(analyze_template_snippet(
        snippet,
        kind,
        imported_bindings,
        locals,
        allow_dollar_prefixed_refs,
    ));
}

pub(super) fn merge_component_tag_usage(
    usage: &mut TemplateUsage,
    tag_name: &str,
    imported_bindings: &FxHashSet<String>,
    locals: &[String],
    allow_kebab_case: bool,
) {
    let tag_name = tag_name.trim();
    if tag_name.is_empty() || imported_bindings.is_empty() {
        return;
    }

    if tag_name.contains('.') {
        merge_expression_usage(usage, tag_name, imported_bindings, locals);
        return;
    }

    mark_binding_used(usage, tag_name, imported_bindings, locals);

    if allow_kebab_case && tag_name.contains('-') {
        let camel = kebab_to_camel_case(tag_name);
        if !camel.is_empty() {
            mark_binding_used(usage, &camel, imported_bindings, locals);
            let pascal = uppercase_first(&camel);
            mark_binding_used(usage, &pascal, imported_bindings, locals);
        }
    }
}

fn mark_binding_used(
    usage: &mut TemplateUsage,
    binding: &str,
    imported_bindings: &FxHashSet<String>,
    locals: &[String],
) {
    if binding.is_empty()
        || locals.iter().any(|local| local == binding)
        || !imported_bindings.contains(binding)
    {
        return;
    }

    usage.used_bindings.insert(binding.to_string());
}

fn kebab_to_camel_case(source: &str) -> String {
    let mut camel = String::new();
    let mut uppercase_next = false;

    for ch in source.chars() {
        if ch == '-' {
            uppercase_next = true;
            continue;
        }

        if uppercase_next {
            camel.extend(ch.to_uppercase());
            uppercase_next = false;
        } else {
            camel.push(ch);
        }
    }

    camel
}

fn uppercase_first(source: &str) -> String {
    let mut chars = source.chars();
    let Some(first) = chars.next() else {
        return String::new();
    };

    let mut output = String::new();
    output.extend(first.to_uppercase());
    output.push_str(chars.as_str());
    output
}

pub(super) fn merge_pattern_binding_usage(
    usage: &mut TemplateUsage,
    pattern: &str,
    imported_bindings: &FxHashSet<String>,
    locals: &[String],
) -> Vec<String> {
    let mut bindings = Vec::new();
    collect_pattern_usage(usage, pattern, imported_bindings, locals, &mut bindings);
    bindings
}

fn collect_pattern_usage(
    usage: &mut TemplateUsage,
    pattern: &str,
    imported_bindings: &FxHashSet<String>,
    locals: &[String],
    bindings: &mut Vec<String>,
) {
    let pattern = trim_outer_parens(pattern.trim());
    let pattern = pattern.strip_prefix("...").unwrap_or(pattern).trim();
    if pattern.is_empty() {
        return;
    }

    // Strip trailing TypeScript type annotations (same as extract_pattern_binding_names).
    let pattern = strip_trailing_type_annotation(pattern);

    if let Some(inner) = strip_wrapping(pattern, '{', '}') {
        for part in split_top_level(inner, ',') {
            let part = part.trim();
            if part.is_empty() || part == "..." {
                continue;
            }
            if let Some((_, rhs)) = split_top_level_once(part, ':') {
                collect_pattern_usage(usage, rhs, imported_bindings, locals, bindings);
                continue;
            }
            if let Some((lhs, rhs)) = split_top_level_once(part, '=') {
                merge_expression_usage(usage, rhs, imported_bindings, locals);
                collect_pattern_usage(usage, lhs, imported_bindings, locals, bindings);
                continue;
            }
            collect_pattern_usage(usage, part, imported_bindings, locals, bindings);
        }
        return;
    }

    if let Some(inner) = strip_wrapping(pattern, '[', ']') {
        for part in split_top_level(inner, ',') {
            collect_pattern_usage(usage, part.trim(), imported_bindings, locals, bindings);
        }
        return;
    }

    if pattern.contains(',') {
        for part in split_top_level(pattern, ',') {
            collect_pattern_usage(usage, part.trim(), imported_bindings, locals, bindings);
        }
        return;
    }

    if let Some((lhs, rhs)) = split_top_level_once(pattern, '=') {
        merge_expression_usage(usage, rhs, imported_bindings, locals);
        collect_pattern_usage(usage, lhs, imported_bindings, locals, bindings);
        return;
    }

    if let Some(ident) = valid_identifier(pattern) {
        bindings.push(ident.to_string());
    }
}

pub(super) fn extract_pattern_binding_names(pattern: &str) -> Vec<String> {
    let pattern = trim_outer_parens(pattern.trim());
    let pattern = pattern.strip_prefix("...").unwrap_or(pattern).trim();
    if pattern.is_empty() {
        return Vec::new();
    }

    // Strip trailing TypeScript type annotations from destructuring patterns.
    // e.g. `{ href, content }: Props` → `{ href, content }`
    //      `[a, b]: number[]`         → `[a, b]`
    // Without this, the pattern falls through to the comma-split path which
    // recurses infinitely because `split_top_level` returns the whole string.
    let pattern = strip_trailing_type_annotation(pattern);

    if let Some(inner) = strip_wrapping(pattern, '{', '}') {
        return split_top_level(inner, ',')
            .into_iter()
            .flat_map(|part| {
                let part = part.trim();
                if part.is_empty() || part == "..." {
                    return Vec::new();
                }
                if let Some((_, rhs)) = split_top_level_once(part, ':') {
                    return extract_pattern_binding_names(rhs);
                }
                if let Some((lhs, _)) = split_top_level_once(part, '=') {
                    return extract_pattern_binding_names(lhs);
                }
                extract_pattern_binding_names(part)
            })
            .collect();
    }

    if let Some(inner) = strip_wrapping(pattern, '[', ']') {
        return split_top_level(inner, ',')
            .into_iter()
            .flat_map(|part| extract_pattern_binding_names(part.trim()))
            .collect();
    }

    if pattern.contains(',') {
        return split_top_level(pattern, ',')
            .into_iter()
            .flat_map(|part| extract_pattern_binding_names(part.trim()))
            .collect();
    }

    if let Some((lhs, _)) = split_top_level_once(pattern, '=') {
        return extract_pattern_binding_names(lhs);
    }

    valid_identifier(pattern)
        .map(|ident| vec![ident.to_string()])
        .unwrap_or_default()
}

fn split_top_level(source: &str, delimiter: char) -> Vec<&str> {
    let mut parts = Vec::new();
    let mut start = 0;
    let mut depth = 0_i32;
    let mut in_single = false;
    let mut in_double = false;
    let mut in_backtick = false;
    let mut escape = false;

    for (idx, ch) in source.char_indices() {
        if escape {
            escape = false;
            continue;
        }
        match ch {
            '\\' if in_single || in_double || in_backtick => {
                escape = true;
            }
            '\'' if !in_double && !in_backtick => in_single = !in_single,
            '"' if !in_single && !in_backtick => in_double = !in_double,
            '`' if !in_single && !in_double => in_backtick = !in_backtick,
            _ if in_single || in_double || in_backtick => {}
            '(' | '[' | '{' => depth += 1,
            ')' | ']' | '}' => depth -= 1,
            _ if ch == delimiter && depth == 0 => {
                parts.push(&source[start..idx]);
                start = idx + ch.len_utf8();
            }
            _ => {}
        }
    }

    parts.push(&source[start..]);
    parts
}

fn split_top_level_once(source: &str, delimiter: char) -> Option<(&str, &str)> {
    let mut depth = 0_i32;
    let mut in_single = false;
    let mut in_double = false;
    let mut in_backtick = false;
    let mut escape = false;

    for (idx, ch) in source.char_indices() {
        if escape {
            escape = false;
            continue;
        }
        match ch {
            '\\' if in_single || in_double || in_backtick => {
                escape = true;
            }
            '\'' if !in_double && !in_backtick => in_single = !in_single,
            '"' if !in_single && !in_backtick => in_double = !in_double,
            '`' if !in_single && !in_double => in_backtick = !in_backtick,
            _ if in_single || in_double || in_backtick => {}
            '(' | '[' | '{' => depth += 1,
            ')' | ']' | '}' => depth -= 1,
            _ if ch == delimiter && depth == 0 => {
                let rhs = &source[idx + ch.len_utf8()..];
                return Some((&source[..idx], rhs));
            }
            _ => {}
        }
    }
    None
}

fn strip_wrapping(source: &str, open: char, close: char) -> Option<&str> {
    source
        .strip_prefix(open)
        .and_then(|inner| inner.strip_suffix(close))
}

/// Strip a trailing TypeScript type annotation from a destructuring pattern.
///
/// Handles patterns like `{ a, b }: SomeType` → `{ a, b }` by finding the
/// matching closing delimiter (`}` or `]`) and discarding everything after it.
/// Returns the input unchanged when there is no trailing annotation.
fn strip_trailing_type_annotation(pattern: &str) -> &str {
    let first = match pattern.bytes().next() {
        Some(b'{') => b'}',
        Some(b'[') => b']',
        _ => return pattern,
    };

    let mut depth: u32 = 0;
    for (i, byte) in pattern.bytes().enumerate() {
        match byte {
            b'{' | b'[' | b'(' => depth += 1,
            b'}' | b']' | b')' => {
                depth -= 1;
                if byte == first && depth == 0 {
                    // Found the matching close; if `: ...` follows, strip it.
                    let rest = pattern[i + 1..].trim_start();
                    if rest.starts_with(':') {
                        return &pattern[..=i];
                    }
                    return pattern;
                }
            }
            _ => {}
        }
    }
    pattern
}

fn trim_outer_parens(source: &str) -> &str {
    source
        .strip_prefix('(')
        .and_then(|inner| inner.strip_suffix(')'))
        .unwrap_or(source)
}

fn valid_identifier(source: &str) -> Option<&str> {
    let mut chars = source.chars();
    let first = chars.next()?;
    if !matches!(first, 'A'..='Z' | 'a'..='z' | '_' | '$') {
        return None;
    }
    chars
        .all(|ch| matches!(ch, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '$'))
        .then_some(source)
}

// ── Shared HTML tag attribute parser ─────────────────────────────

/// A parsed HTML/SFC tag with its name, attributes, and self-closing status.
#[derive(Debug)]
pub(super) struct ParsedTag {
    pub name: String,
    pub attrs: Vec<ParsedAttr>,
    pub self_closing: bool,
}

/// A single attribute on an HTML/SFC tag.
#[derive(Debug)]
pub(super) struct ParsedAttr {
    pub name: String,
    pub value: Option<String>,
}

/// Parse an HTML tag string into its name, attributes, and self-closing status.
///
/// When `braced_values` is true, attribute values like `={expr}` are handled
/// (Svelte syntax). When false, only quoted and unquoted values are recognized (Vue).
pub(super) fn parse_tag_attrs(tag: &str, braced_values: bool) -> ParsedTag {
    let inner = tag.trim_start_matches('<').trim_end_matches('>').trim();
    let self_closing = inner.ends_with('/');
    let inner = inner.trim_end_matches('/').trim_end();

    let name_end = inner
        .char_indices()
        .find_map(|(idx, ch)| ch.is_whitespace().then_some(idx))
        .unwrap_or(inner.len());
    let name = inner[..name_end].trim().to_string();

    let mut attrs = Vec::new();
    let mut index = name_end;

    while index < inner.len() {
        let remaining = &inner[index..];
        let trimmed = remaining.trim_start();
        index += remaining.len() - trimmed.len();
        if index >= inner.len() {
            break;
        }

        let name_end = inner[index..]
            .char_indices()
            .find_map(|(offset, ch)| (ch.is_whitespace() || ch == '=').then_some(index + offset))
            .unwrap_or(inner.len());
        let attr_name = inner[index..name_end].trim();
        index = name_end;

        let remaining = &inner[index..];
        let trimmed = remaining.trim_start();
        index += remaining.len() - trimmed.len();

        let mut value = None;
        if inner.as_bytes().get(index) == Some(&b'=') {
            index += 1;
            let remaining = &inner[index..];
            let trimmed = remaining.trim_start();
            index += remaining.len() - trimmed.len();
            if let Some(quote) = inner.as_bytes().get(index).copied() {
                if quote == b'\'' || quote == b'"' {
                    let quote = quote as char;
                    index += 1;
                    let value_start = index;
                    while index < inner.len() && inner.as_bytes()[index] as char != quote {
                        index += 1;
                    }
                    value = Some(inner[value_start..index].to_string());
                    if index < inner.len() {
                        index += 1;
                    }
                } else if braced_values && quote == b'{' {
                    let Some((expr, next_index)) = scan_curly_section(inner, index, 1, 1) else {
                        break;
                    };
                    value = Some(format!("{{{expr}}}"));
                    index = next_index;
                } else {
                    let value_end = inner[index..]
                        .char_indices()
                        .find_map(|(offset, ch)| ch.is_whitespace().then_some(index + offset))
                        .unwrap_or(inner.len());
                    value = Some(inner[index..value_end].to_string());
                    index = value_end;
                }
            }
        }

        if !attr_name.is_empty() {
            attrs.push(ParsedAttr {
                name: attr_name.to_string(),
                value,
            });
        }
    }

    ParsedTag {
        name,
        attrs,
        self_closing,
    }
}

#[cfg(test)]
mod tests {
    use rustc_hash::FxHashSet;

    use super::{
        extract_pattern_binding_names, kebab_to_camel_case, merge_component_tag_usage,
        merge_expression_usage, merge_expression_usage_allow_dollar_refs,
        merge_pattern_binding_usage, merge_statement_usage,
        merge_statement_usage_allow_dollar_refs, split_top_level, split_top_level_once,
        strip_trailing_type_annotation, strip_wrapping, trim_outer_parens, uppercase_first,
        valid_identifier,
    };
    use crate::template_usage::TemplateUsage;

    // --- extract_pattern_binding_names ---

    #[test]
    fn extracts_nested_object_pattern_bindings() {
        assert_eq!(
            extract_pattern_binding_names("{ item: { id, label }, count = 0 }"),
            vec!["id", "label", "count"],
        );
    }

    #[test]
    fn extracts_array_pattern_bindings() {
        assert_eq!(
            extract_pattern_binding_names("[first, , { value: second }, ...rest]"),
            vec!["first", "second", "rest"],
        );
    }

    #[test]
    fn extracts_comma_separated_parameters() {
        assert_eq!(
            extract_pattern_binding_names("item, index = 0"),
            vec!["item", "index"],
        );
    }

    #[test]
    fn extract_pattern_empty_string_returns_empty() {
        assert!(extract_pattern_binding_names("").is_empty());
    }

    #[test]
    fn extract_pattern_only_spread_returns_empty() {
        assert!(extract_pattern_binding_names("...").is_empty());
    }

    #[test]
    fn extract_pattern_spread_prefix_extracts_binding() {
        assert_eq!(extract_pattern_binding_names("...rest"), vec!["rest"]);
    }

    #[test]
    fn extract_pattern_with_outer_parens() {
        assert_eq!(
            extract_pattern_binding_names("(item, idx)"),
            vec!["item", "idx"],
        );
    }

    #[test]
    fn extract_pattern_invalid_identifier_returns_empty() {
        assert!(extract_pattern_binding_names("123invalid").is_empty());
    }

    #[test]
    fn extract_pattern_object_with_empty_parts() {
        assert_eq!(extract_pattern_binding_names("{ a, , b }"), vec!["a", "b"],);
    }

    #[test]
    fn extract_pattern_object_with_rest_spread() {
        assert_eq!(extract_pattern_binding_names("{ a, ... }"), vec!["a"],);
    }

    #[test]
    fn extract_pattern_top_level_default_value() {
        assert_eq!(extract_pattern_binding_names("x = 42"), vec!["x"],);
    }

    // --- merge_pattern_binding_usage ---

    #[test]
    fn pattern_usage_tracks_default_initializer_references() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["fallbackItem".to_string()]);

        let locals = merge_pattern_binding_usage(
            &mut usage,
            "{ item = fallbackItem }",
            &imported_bindings,
            &[],
        );

        assert_eq!(locals, vec!["item"]);
        assert!(usage.used_bindings.contains("fallbackItem"));
    }

    #[test]
    fn pattern_usage_empty_pattern_returns_no_bindings() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["foo".to_string()]);

        let locals = merge_pattern_binding_usage(&mut usage, "", &imported_bindings, &[]);

        assert!(locals.is_empty());
        assert!(usage.used_bindings.is_empty());
    }

    #[test]
    fn pattern_usage_top_level_default_tracks_reference() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["defaultVal".to_string()]);

        let locals =
            merge_pattern_binding_usage(&mut usage, "x = defaultVal", &imported_bindings, &[]);

        assert_eq!(locals, vec!["x"]);
        assert!(usage.used_bindings.contains("defaultVal"));
    }

    #[test]
    fn pattern_usage_array_with_nested_defaults() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["fallback".to_string()]);

        let locals =
            merge_pattern_binding_usage(&mut usage, "[a, b = fallback]", &imported_bindings, &[]);

        assert_eq!(locals, vec!["a", "b"]);
        assert!(usage.used_bindings.contains("fallback"));
    }

    #[test]
    fn pattern_usage_typed_destructure_does_not_infinite_recurse() {
        // Regression: `{ id, name }: Item` caused infinite recursion via
        // the same mechanism as extract_pattern_binding_names.
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["id".to_string(), "name".to_string()]);

        let locals =
            merge_pattern_binding_usage(&mut usage, "{ id, name }: Item", &imported_bindings, &[]);

        // id and name become locals (shadowing imports)
        assert_eq!(locals.len(), 2);
        assert!(locals.contains(&"id".to_string()));
        assert!(locals.contains(&"name".to_string()));
    }

    #[test]
    fn pattern_usage_typed_array_destructure() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["a".to_string()]);

        let locals =
            merge_pattern_binding_usage(&mut usage, "[a, b]: number[]", &imported_bindings, &[]);

        assert_eq!(locals.len(), 2);
        assert!(locals.contains(&"a".to_string()));
        assert!(locals.contains(&"b".to_string()));
    }

    // --- merge_component_tag_usage ---

    #[test]
    fn component_tag_usage_marks_exact_binding_used() {
        let mut usage = TemplateUsage::default();
        let imported_bindings =
            FxHashSet::from_iter(["GreetingCard".to_string(), "AlertBox".to_string()]);

        merge_component_tag_usage(&mut usage, "GreetingCard", &imported_bindings, &[], false);

        assert!(usage.used_bindings.contains("GreetingCard"));
        assert!(!usage.used_bindings.contains("AlertBox"));
    }

    #[test]
    fn component_tag_usage_converts_kebab_case_for_vue() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["MyButton".to_string()]);

        merge_component_tag_usage(&mut usage, "my-button", &imported_bindings, &[], true);

        assert!(usage.used_bindings.contains("MyButton"));
    }

    #[test]
    fn component_tag_usage_respects_shadowing_locals() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["Item".to_string()]);

        merge_component_tag_usage(
            &mut usage,
            "Item",
            &imported_bindings,
            &["Item".to_string()],
            false,
        );

        assert!(usage.used_bindings.is_empty());
    }

    #[test]
    fn component_tag_usage_tracks_namespaced_members() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["icons".to_string()]);

        merge_component_tag_usage(&mut usage, "icons.Alert", &imported_bindings, &[], false);

        assert!(usage.used_bindings.contains("icons"));
        assert_eq!(usage.member_accesses.len(), 1);
        assert_eq!(usage.member_accesses[0].object, "icons");
        assert_eq!(usage.member_accesses[0].member, "Alert");
    }

    #[test]
    fn component_tag_usage_empty_tag_is_noop() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["Foo".to_string()]);

        merge_component_tag_usage(&mut usage, "", &imported_bindings, &[], false);

        assert!(usage.used_bindings.is_empty());
    }

    #[test]
    fn component_tag_usage_whitespace_only_tag_is_noop() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["Foo".to_string()]);

        merge_component_tag_usage(&mut usage, "  \t  ", &imported_bindings, &[], false);

        assert!(usage.used_bindings.is_empty());
    }

    #[test]
    fn component_tag_usage_empty_bindings_is_noop() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::default();

        merge_component_tag_usage(&mut usage, "MyComponent", &imported_bindings, &[], true);

        assert!(usage.used_bindings.is_empty());
    }

    #[test]
    fn component_tag_kebab_without_allow_flag_skips_conversion() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["MyButton".to_string()]);

        merge_component_tag_usage(&mut usage, "my-button", &imported_bindings, &[], false);

        assert!(!usage.used_bindings.contains("MyButton"));
    }

    #[test]
    fn component_tag_kebab_also_tries_camel_case() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["myButton".to_string()]);

        merge_component_tag_usage(&mut usage, "my-button", &imported_bindings, &[], true);

        assert!(usage.used_bindings.contains("myButton"));
    }

    #[test]
    fn component_tag_binding_not_imported_is_noop() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["OtherComponent".to_string()]);

        merge_component_tag_usage(&mut usage, "Missing", &imported_bindings, &[], false);

        assert!(usage.used_bindings.is_empty());
    }

    // --- merge_expression_usage ---

    #[test]
    fn expression_usage_marks_imported_binding() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["formatDate".to_string()]);

        merge_expression_usage(&mut usage, "formatDate(x)", &imported_bindings, &[]);

        assert!(usage.used_bindings.contains("formatDate"));
    }

    // --- merge_statement_usage ---

    #[test]
    fn statement_usage_marks_imported_binding() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["doSomething".to_string()]);

        merge_statement_usage(&mut usage, "doSomething();", &imported_bindings, &[]);

        assert!(usage.used_bindings.contains("doSomething"));
    }

    // --- merge_expression_usage_allow_dollar_refs ---

    #[test]
    fn expression_usage_dollar_refs_resolves_store_binding() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["count".to_string()]);

        merge_expression_usage_allow_dollar_refs(&mut usage, "$count + 1", &imported_bindings, &[]);

        assert!(usage.used_bindings.contains("count"));
    }

    // --- merge_statement_usage_allow_dollar_refs ---

    #[test]
    fn statement_usage_dollar_refs_resolves_store_binding() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter(["store".to_string()]);

        merge_statement_usage_allow_dollar_refs(
            &mut usage,
            "$store.update();",
            &imported_bindings,
            &[],
        );

        assert!(usage.used_bindings.contains("store"));
    }

    // --- kebab_to_camel_case ---

    #[test]
    fn kebab_to_camel_basic() {
        assert_eq!(kebab_to_camel_case("my-button"), "myButton");
    }

    #[test]
    fn kebab_to_camel_multiple_segments() {
        assert_eq!(
            kebab_to_camel_case("my-long-component-name"),
            "myLongComponentName",
        );
    }

    #[test]
    fn kebab_to_camel_no_dashes() {
        assert_eq!(kebab_to_camel_case("button"), "button");
    }

    #[test]
    fn kebab_to_camel_leading_dash() {
        assert_eq!(kebab_to_camel_case("-button"), "Button");
    }

    #[test]
    fn kebab_to_camel_trailing_dash() {
        assert_eq!(kebab_to_camel_case("button-"), "button");
    }

    #[test]
    fn kebab_to_camel_only_dashes_returns_empty() {
        assert_eq!(kebab_to_camel_case("---"), "");
    }

    #[test]
    fn kebab_to_camel_empty_string() {
        assert_eq!(kebab_to_camel_case(""), "");
    }

    // --- uppercase_first ---

    #[test]
    fn uppercase_first_basic() {
        assert_eq!(uppercase_first("hello"), "Hello");
    }

    #[test]
    fn uppercase_first_already_uppercase() {
        assert_eq!(uppercase_first("Hello"), "Hello");
    }

    #[test]
    fn uppercase_first_empty_string() {
        assert_eq!(uppercase_first(""), "");
    }

    #[test]
    fn uppercase_first_single_char() {
        assert_eq!(uppercase_first("a"), "A");
    }

    // --- valid_identifier ---

    #[test]
    fn valid_identifier_simple() {
        assert_eq!(valid_identifier("foo"), Some("foo"));
    }

    #[test]
    fn valid_identifier_with_underscore() {
        assert_eq!(valid_identifier("_private"), Some("_private"));
    }

    #[test]
    fn valid_identifier_with_dollar() {
        assert_eq!(valid_identifier("$store"), Some("$store"));
    }

    #[test]
    fn valid_identifier_with_digits() {
        assert_eq!(valid_identifier("item2"), Some("item2"));
    }

    #[test]
    fn valid_identifier_starts_with_digit() {
        assert_eq!(valid_identifier("2item"), None);
    }

    #[test]
    fn valid_identifier_empty() {
        assert_eq!(valid_identifier(""), None);
    }

    #[test]
    fn valid_identifier_contains_dash() {
        assert_eq!(valid_identifier("my-var"), None);
    }

    #[test]
    fn valid_identifier_contains_space() {
        assert_eq!(valid_identifier("my var"), None);
    }

    // --- trim_outer_parens ---

    #[test]
    fn trim_parens_removes_outer() {
        assert_eq!(trim_outer_parens("(foo)"), "foo");
    }

    #[test]
    fn trim_parens_no_parens() {
        assert_eq!(trim_outer_parens("foo"), "foo");
    }

    #[test]
    fn trim_parens_only_opening() {
        assert_eq!(trim_outer_parens("(foo"), "(foo");
    }

    #[test]
    fn trim_parens_only_closing() {
        assert_eq!(trim_outer_parens("foo)"), "foo)");
    }

    #[test]
    fn trim_parens_empty() {
        assert_eq!(trim_outer_parens(""), "");
    }

    // --- strip_wrapping ---

    #[test]
    fn strip_wrapping_curly() {
        assert_eq!(strip_wrapping("{ a, b }", '{', '}'), Some(" a, b "));
    }

    #[test]
    fn strip_wrapping_square() {
        assert_eq!(strip_wrapping("[x, y]", '[', ']'), Some("x, y"));
    }

    #[test]
    fn strip_wrapping_no_match() {
        assert_eq!(strip_wrapping("a, b", '{', '}'), None);
    }

    #[test]
    fn strip_wrapping_mismatched() {
        assert_eq!(strip_wrapping("{a, b", '{', '}'), None);
    }

    // --- strip_trailing_type_annotation ---

    #[test]
    fn strip_type_from_object_destructure() {
        assert_eq!(
            strip_trailing_type_annotation("{ href, content }: Props"),
            "{ href, content }"
        );
    }

    #[test]
    fn strip_type_from_array_destructure() {
        assert_eq!(strip_trailing_type_annotation("[a, b]: number[]"), "[a, b]");
    }

    #[test]
    fn strip_type_preserves_plain_destructure() {
        assert_eq!(
            strip_trailing_type_annotation("{ href, content }"),
            "{ href, content }"
        );
    }

    #[test]
    fn strip_type_preserves_identifier() {
        assert_eq!(strip_trailing_type_annotation("item"), "item");
    }

    #[test]
    fn strip_type_nested_braces() {
        assert_eq!(
            strip_trailing_type_annotation("{ a: { b, c } }: Type"),
            "{ a: { b, c } }"
        );
    }

    // --- split_top_level ---

    #[test]
    fn split_top_level_simple() {
        assert_eq!(split_top_level("a, b, c", ','), vec!["a", " b", " c"]);
    }

    #[test]
    fn split_top_level_respects_nested_braces() {
        assert_eq!(split_top_level("{ a, b }, c", ','), vec!["{ a, b }", " c"],);
    }

    #[test]
    fn split_top_level_respects_nested_brackets() {
        assert_eq!(split_top_level("[a, b], c", ','), vec!["[a, b]", " c"],);
    }

    #[test]
    fn split_top_level_respects_nested_parens() {
        assert_eq!(split_top_level("fn(a, b), c", ','), vec!["fn(a, b)", " c"],);
    }

    #[test]
    fn split_top_level_respects_single_quotes() {
        assert_eq!(split_top_level("'a,b', c", ','), vec!["'a,b'", " c"],);
    }

    #[test]
    fn split_top_level_respects_double_quotes() {
        assert_eq!(split_top_level(r#""a,b", c"#, ','), vec![r#""a,b""#, " c"],);
    }

    #[test]
    fn split_top_level_respects_backticks() {
        assert_eq!(split_top_level("`a,b`, c", ','), vec!["`a,b`", " c"],);
    }

    #[test]
    fn split_top_level_respects_escape_in_string() {
        assert_eq!(split_top_level(r"'a\',b', c", ','), vec![r"'a\',b'", " c"],);
    }

    #[test]
    fn split_top_level_no_delimiter() {
        assert_eq!(split_top_level("abc", ','), vec!["abc"]);
    }

    // --- split_top_level_once ---

    #[test]
    fn split_top_level_once_simple() {
        assert_eq!(
            split_top_level_once("key: value", ':'),
            Some(("key", " value")),
        );
    }

    #[test]
    fn split_top_level_once_nested() {
        assert_eq!(
            split_top_level_once("{ a: b }: c", ':'),
            Some(("{ a: b }", " c")),
        );
    }

    #[test]
    fn split_top_level_once_no_delimiter() {
        assert_eq!(split_top_level_once("abc", ':'), None);
    }

    #[test]
    fn split_top_level_once_delimiter_in_single_quotes() {
        assert_eq!(split_top_level_once("'a:b': c", ':'), Some(("'a:b'", " c")),);
    }

    #[test]
    fn split_top_level_once_delimiter_in_double_quotes() {
        assert_eq!(
            split_top_level_once(r#""a:b": c"#, ':'),
            Some((r#""a:b""#, " c")),
        );
    }

    #[test]
    fn split_top_level_once_delimiter_in_backticks() {
        assert_eq!(split_top_level_once("`a:b`: c", ':'), Some(("`a:b`", " c")),);
    }

    #[test]
    fn split_top_level_once_escape_in_string() {
        assert_eq!(
            split_top_level_once(r"'a\':b': c", ':'),
            Some((r"'a\':b'", " c")),
        );
    }

    // --- mark_binding_used edge cases ---

    #[test]
    fn component_tag_kebab_all_dashes_does_not_mark_empty() {
        let mut usage = TemplateUsage::default();
        let imported_bindings = FxHashSet::from_iter([String::new()]);

        merge_component_tag_usage(&mut usage, "---", &imported_bindings, &[], true);

        // kebab_to_camel_case("---") returns "" which is empty, so no conversion marking
        assert!(usage.used_bindings.is_empty());
    }
}
