use std::collections::HashMap;
use std::path::{Path, PathBuf};

use tracing::warn;
use tree_sitter::Language;

use astmap_core::SymbolKind;

use super::{
    collect_type_refs_by_kind, find_enclosing_symbol, node_text, parse_with_tree_sitter,
    symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol, LanguageParser,
    LanguageResolver, LanguageSupport, ParseResult,
};

pub(super) fn lang() -> LanguageSupport {
    LanguageSupport {
        name: "typescript",
        extensions: &["ts", "tsx"],
        parser: &TypeScriptParser,
        resolver_factory: |root| Box::new(TypeScriptResolver::new(root)),
        config_files: &["tsconfig.json"],
        sibling_fn: |_| None, // TypeScript has no sibling expansion
    }
}

// ── Parser ──

pub(crate) struct TypeScriptParser;

impl LanguageParser for TypeScriptParser {
    fn parse(&self, source: &str, file_path: &Path) -> ParseResult {
        let language = if file_path.extension().is_some_and(|e| e == "tsx") {
            tsx_language()
        } else {
            ts_language()
        };
        parse_ts(source, language)
    }

    fn language_name(&self) -> &str {
        "typescript"
    }
}

fn ts_language() -> Language {
    tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
}

fn tsx_language() -> Language {
    tree_sitter_typescript::LANGUAGE_TSX.into()
}

fn parse_ts(source: &str, language: Language) -> ParseResult {
    parse_with_tree_sitter(
        source,
        &language,
        |root, src, symbols, imports| extract_from_node(root, src, symbols, imports, None),
        extract_calls,
        |root, src, symbols| {
            collect_type_refs_by_kind(root, src, symbols, "type_identifier", &is_ts_builtin_type)
        },
    )
}

fn extract_from_node(
    node: tree_sitter::Node,
    source: &str,
    symbols: &mut Vec<ExtractedSymbol>,
    imports: &mut Vec<ExtractedImport>,
    parent_index: Option<usize>,
) {
    let kind = node.kind();

    match kind {
        "function_declaration" => {
            if let Some(sym) = extract_function_or_component(&node, source, parent_index) {
                symbols.push(sym);
                return;
            }
        }
        "class_declaration" => {
            if let Some(sym) = extract_class(&node, source, parent_index) {
                let idx = symbols.len();
                symbols.push(sym);
                // Recurse into class body for methods
                if let Some(body) = node.child_by_field_name("body") {
                    for i in 0..body.child_count() {
                        if let Some(child) = body.child(i as u32) {
                            extract_from_node(child, source, symbols, imports, Some(idx));
                        }
                    }
                }
                return;
            }
        }
        "method_definition" => {
            if let Some(sym) = extract_method(&node, source, parent_index) {
                symbols.push(sym);
                return;
            }
        }
        "interface_declaration" => {
            if let Some(sym) =
                extract_named_symbol(&node, source, SymbolKind::Interface, parent_index)
            {
                symbols.push(sym);
                return;
            }
        }
        "type_alias_declaration" => {
            if let Some(sym) =
                extract_named_symbol(&node, source, SymbolKind::TypeAlias, parent_index)
            {
                symbols.push(sym);
                return;
            }
        }
        "enum_declaration" => {
            if let Some(sym) = extract_named_symbol(&node, source, SymbolKind::Enum, parent_index) {
                symbols.push(sym);
                return;
            }
        }
        "export_statement" => {
            for i in 0..node.child_count() {
                if let Some(child) = node.child(i as u32) {
                    extract_from_node(child, source, symbols, imports, parent_index);
                }
            }
            return;
        }
        "import_statement" => {
            if let Some(imp) = extract_import(&node, source) {
                imports.push(imp);
            }
            return;
        }
        "lexical_declaration" => {
            let is_exported = node
                .parent()
                .is_some_and(|p| p.kind() == "export_statement");
            for i in 0..node.child_count() {
                if let Some(child) = node.child(i as u32) {
                    if child.kind() == "variable_declarator" {
                        if let Some(sym) =
                            extract_arrow_function_symbol(&child, source, parent_index, is_exported)
                        {
                            symbols.push(sym);
                        } else if is_exported {
                            if let Some(sym) = extract_variable_symbol(&child, source, parent_index)
                            {
                                symbols.push(sym);
                            }
                        }
                    }
                }
            }
            return;
        }
        _ => {}
    }

    // Recurse into children
    for i in 0..node.child_count() {
        if let Some(child) = node.child(i as u32) {
            extract_from_node(child, source, symbols, imports, parent_index);
        }
    }
}

fn extract_function_or_component(
    node: &tree_sitter::Node,
    source: &str,
    parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
    let name_node = node.child_by_field_name("name")?;
    let name = node_text(&name_node, source);
    let is_component = is_pascal_case(&name) && body_returns_jsx(node, source);

    let kind = if parent_index.is_some() {
        SymbolKind::Method
    } else if is_component {
        SymbolKind::Component
    } else {
        SymbolKind::Function
    };

    Some(symbol_from_node(name, kind, node, source, parent_index))
}

fn extract_class(
    node: &tree_sitter::Node,
    source: &str,
    parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
    let name_node = node.child_by_field_name("name")?;
    let name = node_text(&name_node, source);

    // Check if it extends React.Component / Component
    let kind = if extends_react_component(node, source) {
        SymbolKind::Component
    } else {
        SymbolKind::Class
    };

    Some(symbol_from_node(name, kind, node, source, parent_index))
}

fn extract_method(
    node: &tree_sitter::Node,
    source: &str,
    parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
    let name_node = node.child_by_field_name("name")?;
    let name = node_text(&name_node, source);
    Some(symbol_from_node(
        name,
        SymbolKind::Method,
        node,
        source,
        parent_index,
    ))
}

fn extract_named_symbol(
    node: &tree_sitter::Node,
    source: &str,
    kind: SymbolKind,
    parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
    let name_node = node.child_by_field_name("name")?;
    let name = node_text(&name_node, source);
    Some(symbol_from_node(name, kind, node, source, parent_index))
}

fn extract_variable_symbol(
    declarator: &tree_sitter::Node,
    source: &str,
    parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
    let name_node = declarator.child_by_field_name("name")?;
    let name = node_text(&name_node, source);

    // Use the outermost statement node (export_statement) for line range
    let parent = declarator.parent()?;
    let top = parent.parent().unwrap_or(parent);

    Some(symbol_from_node(
        name,
        SymbolKind::Variable,
        &top,
        source,
        parent_index,
    ))
}

fn extract_arrow_function_symbol(
    declarator: &tree_sitter::Node,
    source: &str,
    parent_index: Option<usize>,
    is_exported: bool,
) -> Option<ExtractedSymbol> {
    let name_node = declarator.child_by_field_name("name")?;
    let name = node_text(&name_node, source);

    let value = declarator.child_by_field_name("value")?;
    if value.kind() != "arrow_function" {
        return None;
    }

    if !is_exported {
        return None;
    }

    let is_component = is_pascal_case(&name) && arrow_returns_jsx(&value, source);

    let kind = if is_component {
        SymbolKind::Component
    } else {
        SymbolKind::Function
    };

    // Use the parent export_statement or lexical_declaration for line range
    let parent = declarator.parent()?;
    let top = parent.parent().unwrap_or(parent);

    Some(symbol_from_node(name, kind, &top, source, parent_index))
}

fn extract_import(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
    let src_node = node.child_by_field_name("source")?;
    let raw = node_text(&src_node, source);
    // Strip quotes: "'./foo'" or '"./foo"' -> "./foo"
    let path = raw.trim_matches(|c| c == '\'' || c == '"').to_string();

    // Extract named imports: import { Foo, Bar } from '...'
    // and default imports: import Foo from '...'
    let imported_symbols: Vec<String> = (0..node.child_count())
        .filter_map(|i| node.child(i as u32))
        .flat_map(|child| match child.kind() {
            "import_clause" => collect_import_names(&child, source),
            "identifier" => vec![node_text(&child, source)],
            _ => Vec::new(),
        })
        .collect();

    Some(ExtractedImport {
        path,
        imported_symbols,
    })
}

fn collect_import_names(node: &tree_sitter::Node, source: &str) -> Vec<String> {
    (0..node.child_count())
        .filter_map(|i| node.child(i as u32))
        .flat_map(|child| match child.kind() {
            "identifier" => vec![node_text(&child, source)],
            "named_imports" => (0..child.child_count())
                .filter_map(|j| child.child(j as u32))
                .filter(|spec| spec.kind() == "import_specifier")
                .filter_map(|spec| {
                    let local = spec
                        .child_by_field_name("alias")
                        .or_else(|| spec.child_by_field_name("name"))
                        .unwrap_or(spec);
                    (local.kind() == "identifier").then(|| node_text(&local, source))
                })
                .collect(),
            "namespace_import" => (0..child.child_count())
                .filter_map(|k| child.child(k as u32))
                .filter(|ns_child| ns_child.kind() == "identifier")
                .map(|ns_child| node_text(&ns_child, source))
                .collect(),
            _ => collect_import_names(&child, source),
        })
        .collect()
}

// --- Call extraction ---

fn extract_calls(
    root: tree_sitter::Node,
    source: &str,
    symbols: &[ExtractedSymbol],
) -> Vec<ExtractedCall> {
    let mut calls = Vec::new();
    collect_calls(root, source, symbols, &mut calls);
    calls
}

fn collect_calls(
    node: tree_sitter::Node,
    source: &str,
    symbols: &[ExtractedSymbol],
    calls: &mut Vec<ExtractedCall>,
) {
    match node.kind() {
        "call_expression" => {
            if let Some(func_node) = node.child_by_field_name("function") {
                let callee_text = node_text(&func_node, source);
                let callee_name = callee_text.rsplit('.').next().unwrap_or(&callee_text);
                let call_line = node.start_position().row + 1;

                if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
                    calls.push(ExtractedCall {
                        caller_name: caller.to_string(),
                        callee_name: callee_name.to_string(),
                        line: call_line,
                    });
                }
            }
        }
        "new_expression" => {
            if let Some(constructor) = node.child_by_field_name("constructor") {
                let callee_name = node_text(&constructor, source);
                let call_line = node.start_position().row + 1;

                if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
                    calls.push(ExtractedCall {
                        caller_name: caller.to_string(),
                        callee_name: callee_name.to_string(),
                        line: call_line,
                    });
                }
            }
        }
        // JSX component references: <UserAvatar /> creates a "renders" dependency
        "jsx_opening_element" | "jsx_self_closing_element" => {
            // Find the tag name child (skip punctuation like < and />)
            for ci in 0..node.child_count() {
                if let Some(tag) = node.child(ci as u32) {
                    if tag.kind() == "identifier" || tag.kind() == "member_expression" {
                        let tag_name = node_text(&tag, source);
                        // Only PascalCase tags are components
                        if is_pascal_case(&tag_name) {
                            let line = node.start_position().row + 1;
                            if let Some(caller) = find_enclosing_symbol(symbols, line) {
                                calls.push(ExtractedCall {
                                    caller_name: caller.to_string(),
                                    callee_name: tag_name,
                                    line,
                                });
                            }
                        }
                        break;
                    }
                }
            }
        }
        _ => {}
    }

    for i in 0..node.child_count() {
        if let Some(child) = node.child(i as u32) {
            collect_calls(child, source, symbols, calls);
        }
    }
}

// --- Helpers ---

fn is_pascal_case(name: &str) -> bool {
    name.starts_with(|c: char| c.is_ascii_uppercase())
}

fn is_ts_builtin_type(name: &str) -> bool {
    matches!(
        name,
        "string"
            | "number"
            | "boolean"
            | "void"
            | "any"
            | "unknown"
            | "never"
            | "null"
            | "undefined"
            | "object"
            | "symbol"
            | "bigint"
            | "Array"
            | "Promise"
            | "Map"
            | "Set"
            | "Record"
            | "Partial"
            | "Required"
            | "Readonly"
            | "Pick"
            | "Omit"
            | "Exclude"
            | "Extract"
            | "ReturnType"
            | "Parameters"
    )
}

fn body_returns_jsx(func_node: &tree_sitter::Node, source: &str) -> bool {
    if let Some(body) = func_node.child_by_field_name("body") {
        return contains_jsx_return(&body, source);
    }
    false
}

fn arrow_returns_jsx(arrow: &tree_sitter::Node, source: &str) -> bool {
    if let Some(body) = arrow.child_by_field_name("body") {
        // Arrow with expression body: () => <div/>
        if is_jsx_node(&body) {
            return true;
        }
        // Arrow with parenthesized expression body: () => (<div/>)
        if body.kind() == "parenthesized_expression" {
            for i in 0..body.child_count() {
                if let Some(child) = body.child(i as u32) {
                    if is_jsx_node(&child) {
                        return true;
                    }
                }
            }
        }
        // Arrow with block body: () => { return <div/> }
        return contains_jsx_return(&body, source);
    }
    false
}

fn contains_jsx_return(node: &tree_sitter::Node, _source: &str) -> bool {
    if node.kind() == "return_statement" {
        for i in 0..node.child_count() {
            if let Some(child) = node.child(i as u32) {
                if is_jsx_node(&child) {
                    return true;
                }
                // Check parenthesized: return (<div/>)
                if child.kind() == "parenthesized_expression" {
                    for j in 0..child.child_count() {
                        if let Some(grandchild) = child.child(j as u32) {
                            if is_jsx_node(&grandchild) {
                                return true;
                            }
                        }
                    }
                }
            }
        }
    }
    // Recurse into block children
    for i in 0..node.child_count() {
        if let Some(child) = node.child(i as u32) {
            if contains_jsx_return(&child, _source) {
                return true;
            }
        }
    }
    false
}

fn is_jsx_node(node: &tree_sitter::Node) -> bool {
    matches!(
        node.kind(),
        "jsx_element" | "jsx_self_closing_element" | "jsx_fragment"
    )
}

fn extends_react_component(class_node: &tree_sitter::Node, source: &str) -> bool {
    // Look for class_heritage / extends clause
    for i in 0..class_node.child_count() {
        if let Some(child) = class_node.child(i as u32) {
            if child.kind() == "class_heritage" {
                let text = node_text(&child, source);
                return text.contains("Component") || text.contains("PureComponent");
            }
        }
    }
    false
}

// ── Resolver ──

/// TypeScript import resolver.
/// Resolves relative paths and tsconfig.json `paths` aliases.
/// External packages (node_modules) return None.
pub(crate) struct TypeScriptResolver {
    /// Cached tsconfig paths mapping: alias pattern -> replacement paths
    /// e.g., "@/*" -> ["./src/*"]
    tsconfig_paths: HashMap<String, Vec<String>>,
    /// Base URL from tsconfig.json (defaults to project root)
    base_url: Option<String>,
}

impl TypeScriptResolver {
    pub fn new(project_root: &Path) -> Self {
        let (tsconfig_paths, base_url) = load_tsconfig_paths(project_root);
        Self {
            tsconfig_paths,
            base_url,
        }
    }
}

impl LanguageResolver for TypeScriptResolver {
    fn resolve_import(
        &self,
        import_path: &str,
        source_file: &str,
        project_root: &Path,
    ) -> Option<PathBuf> {
        if import_path.is_empty() {
            return None;
        }

        // Relative imports: ./foo or ../foo
        if import_path.starts_with("./") || import_path.starts_with("../") {
            let source_abs = project_root.join(source_file);
            let source_dir = source_abs.parent()?;
            let target = source_dir.join(import_path);
            return resolve_ts_file(&target);
        }

        // tsconfig paths aliases
        if let Some(resolved) = self.resolve_via_tsconfig(import_path, project_root) {
            return Some(resolved);
        }

        // External package (no ./ or ../ and not a configured alias)
        None
    }
}

impl TypeScriptResolver {
    fn resolve_via_tsconfig(&self, import_path: &str, project_root: &Path) -> Option<PathBuf> {
        let base = if let Some(ref base_url) = self.base_url {
            project_root.join(base_url)
        } else {
            project_root.to_path_buf()
        };

        for (pattern, replacements) in &self.tsconfig_paths {
            if let Some(matched) = match_tsconfig_pattern(pattern, import_path) {
                for replacement in replacements {
                    let resolved_path = replacement.replace('*', &matched);
                    let target = base.join(&resolved_path);
                    if let Some(file) = resolve_ts_file(&target) {
                        return Some(file);
                    }
                }
            }
        }

        None
    }
}

/// Try to resolve a path to an actual TypeScript file.
/// Resolution order: path.ts, path.tsx, path/index.ts, path/index.tsx
/// Returns a canonicalized path to ensure consistent path comparisons.
fn resolve_ts_file(target: &Path) -> Option<PathBuf> {
    // Direct match (already has extension)
    if target.exists() && target.is_file() {
        return target.canonicalize().ok();
    }

    // Try .ts extension
    let ts = target.with_extension("ts");
    if ts.exists() {
        return ts.canonicalize().ok();
    }

    // Try .tsx extension
    let tsx = target.with_extension("tsx");
    if tsx.exists() {
        return tsx.canonicalize().ok();
    }

    // Try index.ts in directory
    let index_ts = target.join("index.ts");
    if index_ts.exists() {
        return index_ts.canonicalize().ok();
    }

    // Try index.tsx in directory
    let index_tsx = target.join("index.tsx");
    if index_tsx.exists() {
        return index_tsx.canonicalize().ok();
    }

    None
}

/// Match a tsconfig path pattern against an import path.
/// Pattern "@ /*" matches "@/foo/bar" and returns "foo/bar".
/// Pattern "utils" matches "utils" exactly and returns "".
fn match_tsconfig_pattern(pattern: &str, import_path: &str) -> Option<String> {
    if let Some(prefix) = pattern.strip_suffix('*') {
        if let Some(rest) = import_path.strip_prefix(prefix) {
            return Some(rest.to_string());
        }
    } else if pattern == import_path {
        return Some(String::new());
    }
    None
}

/// Load paths configuration from tsconfig.json at project root.
/// Returns empty map if tsconfig.json doesn't exist or is invalid.
fn load_tsconfig_paths(project_root: &Path) -> (HashMap<String, Vec<String>>, Option<String>) {
    let tsconfig_path = project_root.join("tsconfig.json");
    if !tsconfig_path.exists() {
        return (HashMap::new(), None);
    }

    let content = match std::fs::read_to_string(&tsconfig_path) {
        Ok(c) => c,
        Err(e) => {
            warn!("cannot read tsconfig.json: {}", e);
            return (HashMap::new(), None);
        }
    };

    let json: serde_json::Value = match serde_json::from_str(&content) {
        Ok(v) => v,
        Err(e) => {
            warn!("cannot parse tsconfig.json: {}", e);
            return (HashMap::new(), None);
        }
    };

    let compiler_options = json.get("compilerOptions");
    let base_url = compiler_options
        .and_then(|co| co.get("baseUrl"))
        .and_then(|v| v.as_str())
        .map(String::from);

    let mut paths = HashMap::new();
    if let Some(paths_obj) = compiler_options
        .and_then(|co| co.get("paths"))
        .and_then(|v| v.as_object())
    {
        for (key, value) in paths_obj {
            if let Some(arr) = value.as_array() {
                let targets: Vec<String> = arr
                    .iter()
                    .filter_map(|v| v.as_str().map(String::from))
                    .collect();
                if !targets.is_empty() {
                    paths.insert(key.clone(), targets);
                }
            }
        }
    }

    (paths, base_url)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::path::PathBuf;

    // --- Parser test helpers ---

    fn parse_ts_source(source: &str) -> ParseResult {
        let parser = TypeScriptParser;
        parser.parse(source, &PathBuf::from("test.ts"))
    }

    fn parse_tsx_source(source: &str) -> ParseResult {
        let parser = TypeScriptParser;
        parser.parse(source, &PathBuf::from("test.tsx"))
    }

    // --- Parser tests ---

    #[test]
    fn test_parse_function() {
        let result =
            parse_ts_source("function greet(name: string): string { return `Hello ${name}`; }");
        assert!(!result.has_errors);
        assert_eq!(result.symbols.len(), 1);
        assert_eq!(result.symbols[0].name, "greet");
        assert_eq!(result.symbols[0].kind, SymbolKind::Function);
    }

    #[test]
    fn test_parse_class_with_methods() {
        let result = parse_ts_source(
            r#"
class UserService {
    private name: string;
    constructor(name: string) {
        this.name = name;
    }
    getName(): string {
        return this.name;
    }
}
"#,
        );
        assert!(!result.has_errors);
        let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
        assert!(names.contains(&"UserService"));
        assert!(names.contains(&"constructor"));
        assert!(names.contains(&"getName"));
        let class_sym = result
            .symbols
            .iter()
            .find(|s| s.name == "UserService")
            .unwrap();
        assert_eq!(class_sym.kind, SymbolKind::Class);
    }

    #[test]
    fn test_parse_interface() {
        let result = parse_ts_source("interface UserProps { name: string; age: number; }");
        assert!(!result.has_errors);
        assert_eq!(result.symbols.len(), 1);
        assert_eq!(result.symbols[0].name, "UserProps");
        assert_eq!(result.symbols[0].kind, SymbolKind::Interface);
    }

    #[test]
    fn test_parse_type_alias() {
        let result = parse_ts_source(
            "type Result<T> = { ok: true; value: T } | { ok: false; error: string };",
        );
        assert!(!result.has_errors);
        assert_eq!(result.symbols.len(), 1);
        assert_eq!(result.symbols[0].name, "Result");
        assert_eq!(result.symbols[0].kind, SymbolKind::TypeAlias);
    }

    #[test]
    fn test_parse_enum() {
        let result = parse_ts_source("enum Direction { Up, Down, Left, Right }");
        assert!(!result.has_errors);
        assert_eq!(result.symbols.len(), 1);
        assert_eq!(result.symbols[0].name, "Direction");
        assert_eq!(result.symbols[0].kind, SymbolKind::Enum);
    }

    #[test]
    fn test_parse_imports() {
        let result = parse_ts_source(
            r#"
import { useState, useEffect } from 'react';
import UserService from './services/UserService';
import type { Config } from '../config';
"#,
        );
        assert_eq!(result.imports.len(), 3);
        assert_eq!(result.imports[0].path, "react");
        assert_eq!(result.imports[1].path, "./services/UserService");
        assert_eq!(result.imports[2].path, "../config");
    }

    #[test]
    fn test_parse_exported_arrow_component() {
        let result = parse_tsx_source(
            r#"
export const Button = ({ label }: ButtonProps) => {
    return <button>{label}</button>;
};
"#,
        );
        assert!(!result.has_errors, "parse errors");
        let button = result.symbols.iter().find(|s| s.name == "Button");
        assert!(button.is_some(), "Button not found in {:?}", result.symbols);
        assert_eq!(button.unwrap().kind, SymbolKind::Component);
    }

    #[test]
    fn test_parse_function_component() {
        let result = parse_tsx_source(
            r#"
export function UserCard({ user }: { user: User }) {
    return (
        <div>
            <span>{user.name}</span>
        </div>
    );
}
"#,
        );
        assert!(!result.has_errors);
        let card = result.symbols.iter().find(|s| s.name == "UserCard");
        assert!(card.is_some(), "UserCard not found in {:?}", result.symbols);
        assert_eq!(card.unwrap().kind, SymbolKind::Component);
    }

    #[test]
    fn test_parse_class_component() {
        let result = parse_tsx_source(
            r#"
class Dashboard extends React.Component<Props> {
    render() {
        return <div>Dashboard</div>;
    }
}
"#,
        );
        assert!(!result.has_errors);
        let dash = result.symbols.iter().find(|s| s.name == "Dashboard");
        assert!(
            dash.is_some(),
            "Dashboard not found in {:?}",
            result.symbols
        );
        assert_eq!(dash.unwrap().kind, SymbolKind::Component);
    }

    #[test]
    fn test_call_extraction() {
        let result = parse_ts_source(
            r#"
function fetchData() { return fetch('/api'); }
function main() {
    const data = fetchData();
    console.log(data);
}
"#,
        );
        let callee_names: Vec<&str> = result
            .calls
            .iter()
            .map(|c| c.callee_name.as_str())
            .collect();
        assert!(
            callee_names.contains(&"fetchData"),
            "calls: {:?}",
            result.calls
        );
    }

    #[test]
    fn test_new_expression() {
        let result = parse_ts_source(
            r#"
class Foo {}
function create() {
    const f = new Foo();
}
"#,
        );
        let callee_names: Vec<&str> = result
            .calls
            .iter()
            .map(|c| c.callee_name.as_str())
            .collect();
        assert!(callee_names.contains(&"Foo"), "calls: {:?}", result.calls);
    }

    #[test]
    fn test_jsx_component_reference() {
        let result = parse_tsx_source(
            r#"
export function App() {
    return (
        <div>
            <UserAvatar />
            <ProfileCard name="test" />
        </div>
    );
}
"#,
        );
        let callee_names: Vec<&str> = result
            .calls
            .iter()
            .map(|c| c.callee_name.as_str())
            .collect();
        assert!(
            callee_names.contains(&"UserAvatar"),
            "calls: {:?}",
            result.calls
        );
        assert!(
            callee_names.contains(&"ProfileCard"),
            "calls: {:?}",
            result.calls
        );
        // HTML tags should not be in calls
        assert!(!callee_names.contains(&"div"));
    }

    #[test]
    fn test_type_ref_extraction() {
        let result = parse_ts_source(
            r#"
interface Config { port: number; }
function createServer(config: Config): void {}
"#,
        );
        let ref_types: Vec<&str> = result
            .type_refs
            .iter()
            .map(|r| r.to_type.as_str())
            .collect();
        assert!(
            ref_types.contains(&"Config"),
            "type_refs: {:?}",
            result.type_refs
        );
    }

    #[test]
    fn test_ts_builtin_types_filtered() {
        let result = parse_ts_source(
            r#"
function test(name: string, age: number, active: boolean): void {}
"#,
        );
        // All types are builtins, so no type_refs should be extracted
        assert!(
            result.type_refs.is_empty(),
            "should filter builtins: {:?}",
            result.type_refs
        );
    }

    #[test]
    fn test_empty_ts_file() {
        let result = parse_ts_source("");
        assert!(!result.has_errors);
        assert_eq!(result.symbols.len(), 0);
    }

    #[test]
    fn test_malformed_ts_file() {
        let result = parse_ts_source("function broken( { }");
        assert!(result.has_errors);
    }

    #[test]
    fn test_import_extracts_named_symbols() {
        let result = parse_ts_source("import { Foo, Bar } from './baz';");
        assert_eq!(result.imports.len(), 1);
        assert_eq!(result.imports[0].imported_symbols, vec!["Foo", "Bar"]);
    }

    #[test]
    fn test_import_extracts_default_symbol() {
        let result = parse_ts_source("import React from 'react';");
        assert_eq!(result.imports.len(), 1);
        assert_eq!(result.imports[0].imported_symbols, vec!["React"]);
    }

    #[test]
    fn test_import_extracts_aliased_symbol() {
        let result = parse_ts_source("import { Foo as Bar } from './baz';");
        assert_eq!(result.imports.len(), 1);
        // Should use the local alias name
        assert!(result.imports[0]
            .imported_symbols
            .contains(&"Bar".to_string()));
    }

    #[test]
    fn test_import_namespace() {
        let result = parse_ts_source("import * as Utils from './utils';");
        assert_eq!(result.imports.len(), 1);
        assert_eq!(result.imports[0].imported_symbols, vec!["Utils"]);
    }

    #[test]
    fn test_export_const_variable() {
        let result = parse_ts_source(
            r#"
import { compile } from 'zod-aot';
import { z } from 'zod';

export const UserSchema = compile(
  z.object({ name: z.string(), age: z.number() }),
);
"#,
        );
        assert!(!result.has_errors);
        let sym = result.symbols.iter().find(|s| s.name == "UserSchema");
        assert!(
            sym.is_some(),
            "UserSchema not found in {:?}",
            result.symbols
        );
        assert_eq!(sym.unwrap().kind, SymbolKind::Variable);
    }

    #[test]
    fn test_export_const_simple_value() {
        let result = parse_ts_source("export const API_URL = 'https://api.example.com';");
        assert!(!result.has_errors);
        let sym = result.symbols.iter().find(|s| s.name == "API_URL");
        assert!(sym.is_some(), "API_URL not found in {:?}", result.symbols);
        assert_eq!(sym.unwrap().kind, SymbolKind::Variable);
    }

    #[test]
    fn test_export_const_object() {
        let result = parse_ts_source("export const config = { port: 3000, host: 'localhost' };");
        assert!(!result.has_errors);
        let sym = result.symbols.iter().find(|s| s.name == "config");
        assert!(sym.is_some(), "config not found in {:?}", result.symbols);
        assert_eq!(sym.unwrap().kind, SymbolKind::Variable);
    }

    #[test]
    fn test_non_exported_const_not_extracted() {
        let result = parse_ts_source("const internal = { port: 3000 };");
        assert!(!result.has_errors);
        let sym = result.symbols.iter().find(|s| s.name == "internal");
        assert!(sym.is_none(), "non-exported const should not be extracted");
    }

    #[test]
    fn test_export_const_arrow_still_function() {
        let result = parse_ts_source("export const add = (a: number, b: number) => a + b;");
        assert!(!result.has_errors);
        let sym = result.symbols.iter().find(|s| s.name == "add");
        assert!(sym.is_some(), "add not found in {:?}", result.symbols);
        assert_eq!(sym.unwrap().kind, SymbolKind::Function);
    }

    #[test]
    fn test_is_pascal_case_positive() {
        assert!(is_pascal_case("Button"));
        assert!(is_pascal_case("FooBar"));
        assert!(is_pascal_case("A"));
        assert!(is_pascal_case("UserAvatar"));
    }

    #[test]
    fn test_is_pascal_case_negative() {
        assert!(!is_pascal_case("button"));
        assert!(!is_pascal_case(""));
        assert!(!is_pascal_case("aButton"));
        assert!(!is_pascal_case("_Private"));
    }

    #[test]
    fn test_is_ts_builtin_type_positive() {
        assert!(is_ts_builtin_type("string"));
        assert!(is_ts_builtin_type("number"));
        assert!(is_ts_builtin_type("boolean"));
        assert!(is_ts_builtin_type("Array"));
        assert!(is_ts_builtin_type("Promise"));
        assert!(is_ts_builtin_type("Record"));
        assert!(is_ts_builtin_type("ReturnType"));
    }

    #[test]
    fn test_is_ts_builtin_type_negative() {
        assert!(!is_ts_builtin_type("MyType"));
        assert!(!is_ts_builtin_type("Config"));
        assert!(!is_ts_builtin_type("UserService"));
        assert!(!is_ts_builtin_type(""));
    }

    #[test]
    fn test_extends_react_component_positive() {
        let result = parse_tsx_source(
            r#"
class MyComponent extends React.Component<Props> {
    render() { return <div/>; }
}
"#,
        );
        let comp = result.symbols.iter().find(|s| s.name == "MyComponent");
        assert!(comp.is_some());
        assert_eq!(comp.unwrap().kind, SymbolKind::Component);
    }

    #[test]
    fn test_extends_pure_component() {
        let result = parse_tsx_source(
            r#"
class MyPure extends React.PureComponent<Props> {
    render() { return <div/>; }
}
"#,
        );
        let comp = result.symbols.iter().find(|s| s.name == "MyPure");
        assert!(comp.is_some());
        assert_eq!(comp.unwrap().kind, SymbolKind::Component);
    }

    #[test]
    fn test_class_without_heritage_is_not_component() {
        let result = parse_tsx_source(
            r#"
class UserService {
    fetch() { return null; }
}
"#,
        );
        let svc = result.symbols.iter().find(|s| s.name == "UserService");
        assert!(svc.is_some());
        assert_eq!(svc.unwrap().kind, SymbolKind::Class);
    }

    #[test]
    fn test_export_const_calls_captured() {
        let result = parse_ts_source(
            r#"
export const Schema = compile(z.object({ name: z.string() }));
"#,
        );
        assert!(!result.has_errors);
        let sym = result.symbols.iter().find(|s| s.name == "Schema");
        assert!(sym.is_some(), "Schema not found");
        // The call to compile should be captured within the Schema symbol
        let compile_calls: Vec<_> = result
            .calls
            .iter()
            .filter(|c| c.callee_name == "compile")
            .collect();
        assert!(
            !compile_calls.is_empty(),
            "compile() call should be captured, calls: {:?}",
            result.calls
        );
        assert_eq!(compile_calls[0].caller_name, "Schema");
    }

    // --- Resolver tests ---

    #[test]
    fn test_resolve_relative_ts() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("utils.ts"), "export function foo() {}").unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("./utils", "src/main.ts", tmp.path());
        assert!(result.is_some(), "should resolve ./utils");
        assert!(result.unwrap().ends_with("utils.ts"));
    }

    #[test]
    fn test_resolve_relative_tsx() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(
            src.join("Button.tsx"),
            "export const Button = () => <button/>;",
        )
        .unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("./Button", "src/App.tsx", tmp.path());
        assert!(result.is_some(), "should resolve ./Button");
        assert!(result.unwrap().ends_with("Button.tsx"));
    }

    #[test]
    fn test_resolve_index_ts() {
        let tmp = tempfile::tempdir().unwrap();
        let components = tmp.path().join("src").join("components");
        fs::create_dir_all(&components).unwrap();
        fs::write(components.join("index.ts"), "export * from './Button';").unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("./components", "src/App.tsx", tmp.path());
        assert!(result.is_some(), "should resolve ./components to index.ts");
        assert!(result.unwrap().ends_with("index.ts"));
    }

    #[test]
    fn test_resolve_parent_relative() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(src.join("components")).unwrap();
        fs::write(src.join("utils.ts"), "").unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("../utils", "src/components/Button.tsx", tmp.path());
        assert!(result.is_some(), "should resolve ../utils");
        assert!(result.unwrap().ends_with("utils.ts"));
    }

    #[test]
    fn test_resolve_tsconfig_paths() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(src.join("components")).unwrap();
        fs::write(src.join("components").join("Button.tsx"), "").unwrap();

        fs::write(
            tmp.path().join("tsconfig.json"),
            r#"{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }"#,
        )
        .unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("@/components/Button", "src/App.tsx", tmp.path());
        assert!(result.is_some(), "should resolve @/components/Button");
        assert!(result.unwrap().ends_with("Button.tsx"));
    }

    #[test]
    fn test_resolve_external_package_returns_none() {
        let tmp = tempfile::tempdir().unwrap();
        let resolver = TypeScriptResolver::new(tmp.path());
        assert_eq!(
            resolver.resolve_import("react", "src/App.tsx", tmp.path()),
            None
        );
        assert_eq!(
            resolver.resolve_import("@tanstack/react-query", "src/App.tsx", tmp.path()),
            None
        );
    }

    #[test]
    fn test_resolve_no_tsconfig() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("foo.ts"), "").unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        // Relative still works without tsconfig
        let result = resolver.resolve_import("./foo", "src/main.ts", tmp.path());
        assert!(result.is_some());
        // Alias doesn't work without tsconfig
        let result = resolver.resolve_import("@/foo", "src/main.ts", tmp.path());
        assert!(result.is_none());
    }

    #[test]
    fn test_resolve_invalid_tsconfig() {
        let tmp = tempfile::tempdir().unwrap();
        fs::write(tmp.path().join("tsconfig.json"), "not json!!!").unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("foo.ts"), "").unwrap();

        // Should not panic, just fall back to no paths
        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("./foo", "src/main.ts", tmp.path());
        assert!(result.is_some(), "relative import should still work");
    }

    #[test]
    fn test_match_tsconfig_pattern() {
        assert_eq!(
            match_tsconfig_pattern("@/*", "@/foo/bar"),
            Some("foo/bar".to_string())
        );
        assert_eq!(match_tsconfig_pattern("@/*", "react"), None);
        assert_eq!(
            match_tsconfig_pattern("utils", "utils"),
            Some(String::new())
        );
        assert_eq!(match_tsconfig_pattern("utils", "utils/foo"), None);
    }

    // --- Additional coverage tests ---

    #[test]
    fn test_parse_arrow_component_expression_body_jsx() {
        // Arrow with expression body returning JSX directly: () => <div/>
        let result = parse_tsx_source(
            r#"
export const Icon = () => <svg><path d="M0 0"/></svg>;
"#,
        );
        assert!(!result.has_errors);
        let icon = result.symbols.iter().find(|s| s.name == "Icon");
        assert!(icon.is_some(), "Icon not found in {:?}", result.symbols);
        assert_eq!(icon.unwrap().kind, SymbolKind::Component);
    }

    #[test]
    fn test_parse_arrow_component_parenthesized_jsx() {
        // Arrow with parenthesized expression body: () => (<div/>)
        let result = parse_tsx_source(
            r#"
export const Badge = () => (<span className="badge">Hi</span>);
"#,
        );
        assert!(!result.has_errors);
        let badge = result.symbols.iter().find(|s| s.name == "Badge");
        assert!(badge.is_some(), "Badge not found in {:?}", result.symbols);
        assert_eq!(badge.unwrap().kind, SymbolKind::Component);
    }

    #[test]
    fn test_parse_function_component_parenthesized_return() {
        // Function component with parenthesized return: return (<div/>)
        let result = parse_tsx_source(
            r#"
export function Layout() {
    return (
        <main>
            <h1>Title</h1>
        </main>
    );
}
"#,
        );
        assert!(!result.has_errors);
        let layout = result.symbols.iter().find(|s| s.name == "Layout");
        assert!(layout.is_some(), "Layout not found in {:?}", result.symbols);
        assert_eq!(layout.unwrap().kind, SymbolKind::Component);
    }

    #[test]
    fn test_non_component_arrow_lowercase() {
        // Lowercase arrow function should NOT be a component even if exported
        let result = parse_tsx_source(
            r#"
export const helper = () => <div/>;
"#,
        );
        assert!(!result.has_errors);
        let helper = result.symbols.iter().find(|s| s.name == "helper");
        assert!(helper.is_some(), "helper not found in {:?}", result.symbols);
        // lowercase = not a component even with JSX
        assert_eq!(helper.unwrap().kind, SymbolKind::Function);
    }

    #[test]
    fn test_class_extends_non_component() {
        // Class that extends something that is NOT Component/PureComponent
        let result = parse_tsx_source(
            r#"
class MyService extends BaseService {
    fetch() { return null; }
}
"#,
        );
        assert!(!result.has_errors);
        let svc = result.symbols.iter().find(|s| s.name == "MyService");
        assert!(svc.is_some());
        assert_eq!(
            svc.unwrap().kind,
            SymbolKind::Class,
            "should be class, not component"
        );
    }

    #[test]
    fn test_resolve_index_tsx() {
        // Resolution should fall through to index.tsx
        let tmp = tempfile::tempdir().unwrap();
        let components = tmp.path().join("src").join("components");
        fs::create_dir_all(&components).unwrap();
        fs::write(
            components.join("index.tsx"),
            "export const Foo = () => <div/>;",
        )
        .unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("./components", "src/App.tsx", tmp.path());
        assert!(result.is_some(), "should resolve ./components to index.tsx");
        assert!(result.unwrap().ends_with("index.tsx"));
    }

    #[test]
    fn test_resolve_tsx_extension() {
        // Direct .tsx resolution
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(
            src.join("Widget.tsx"),
            "export const Widget = () => <div/>;",
        )
        .unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("./Widget", "src/App.tsx", tmp.path());
        assert!(result.is_some(), "should resolve ./Widget to .tsx");
        assert!(result.unwrap().ends_with("Widget.tsx"));
    }

    #[test]
    fn test_resolve_empty_import_path() {
        let tmp = tempfile::tempdir().unwrap();
        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("", "src/App.tsx", tmp.path());
        assert!(result.is_none(), "empty import path should return None");
    }

    #[test]
    fn test_resolve_tsconfig_with_base_url_only() {
        // tsconfig with baseUrl but no paths
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("utils.ts"), "export function foo() {}").unwrap();

        fs::write(
            tmp.path().join("tsconfig.json"),
            r#"{ "compilerOptions": { "baseUrl": "." } }"#,
        )
        .unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        // Without paths config, non-relative non-alias imports return None
        let result = resolver.resolve_import("src/utils", "src/App.tsx", tmp.path());
        assert!(result.is_none());
    }

    #[test]
    fn test_resolve_tsconfig_exact_match_pattern() {
        // tsconfig with exact (non-wildcard) path alias
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("config.ts"), "export const config = {};").unwrap();

        fs::write(
            tmp.path().join("tsconfig.json"),
            r#"{ "compilerOptions": { "baseUrl": ".", "paths": { "config": ["./src/config"] } } }"#,
        )
        .unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("config", "src/App.tsx", tmp.path());
        assert!(result.is_some(), "exact tsconfig pattern should resolve");
        assert!(result.unwrap().ends_with("config.ts"));
    }

    #[test]
    fn test_parse_jsx_fragment() {
        // JSX fragment: <><Child/></>
        let result = parse_tsx_source(
            r#"
export function Wrapper() {
    return (
        <>
            <ChildComp />
        </>
    );
}
"#,
        );
        assert!(!result.has_errors);
        let wrapper = result.symbols.iter().find(|s| s.name == "Wrapper");
        assert!(wrapper.is_some());
        assert_eq!(wrapper.unwrap().kind, SymbolKind::Component);
        // ChildComp should be in calls
        let callee_names: Vec<&str> = result
            .calls
            .iter()
            .map(|c| c.callee_name.as_str())
            .collect();
        assert!(
            callee_names.contains(&"ChildComp"),
            "calls: {:?}",
            result.calls
        );
    }

    #[test]
    fn test_parse_namespace_import() {
        // import * as Foo from './bar'
        let result = parse_ts_source("import * as Helpers from './helpers';");
        assert_eq!(result.imports.len(), 1);
        assert_eq!(result.imports[0].imported_symbols, vec!["Helpers"]);
    }

    #[test]
    fn test_parse_default_and_named_imports() {
        let result = parse_ts_source("import React, { useState, useEffect } from 'react';");
        assert_eq!(result.imports.len(), 1);
        let names = &result.imports[0].imported_symbols;
        assert!(names.contains(&"React".to_string()), "imports: {:?}", names);
        assert!(
            names.contains(&"useState".to_string()),
            "imports: {:?}",
            names
        );
        assert!(
            names.contains(&"useEffect".to_string()),
            "imports: {:?}",
            names
        );
    }

    #[test]
    fn test_load_tsconfig_no_paths_key() {
        // tsconfig.json exists but has no "paths" key
        let tmp = tempfile::tempdir().unwrap();
        fs::write(
            tmp.path().join("tsconfig.json"),
            r#"{ "compilerOptions": { "strict": true } }"#,
        )
        .unwrap();

        let (paths, base_url) = load_tsconfig_paths(tmp.path());
        assert!(paths.is_empty());
        assert!(base_url.is_none());
    }

    #[test]
    fn test_load_tsconfig_no_compiler_options() {
        // tsconfig.json with no compilerOptions at all
        let tmp = tempfile::tempdir().unwrap();
        fs::write(
            tmp.path().join("tsconfig.json"),
            r#"{ "include": ["src"] }"#,
        )
        .unwrap();

        let (paths, base_url) = load_tsconfig_paths(tmp.path());
        assert!(paths.is_empty());
        assert!(base_url.is_none());
    }

    #[test]
    fn test_arrow_function_non_exported_not_extracted() {
        let result = parse_ts_source("const helper = () => 42;");
        assert!(!result.has_errors);
        let sym = result.symbols.iter().find(|s| s.name == "helper");
        assert!(
            sym.is_none(),
            "non-exported arrow function should not be extracted"
        );
    }

    #[test]
    fn test_lexical_declaration_non_arrow_value() {
        let result = parse_ts_source("const x = 42;");
        assert!(!result.has_errors);
        assert!(
            result.symbols.is_empty(),
            "non-exported non-arrow const should not be extracted"
        );
    }

    #[test]
    fn test_method_in_class_is_method_kind() {
        let result = parse_ts_source(
            r#"
class Service {
    async fetch(): Promise<void> {}
    private handle(): void {}
}
"#,
        );
        assert!(!result.has_errors);
        let methods: Vec<&str> = result
            .symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Method)
            .map(|s| s.name.as_str())
            .collect();
        assert!(methods.contains(&"fetch"), "methods: {:?}", methods);
        assert!(methods.contains(&"handle"), "methods: {:?}", methods);
    }

    #[test]
    fn test_body_returns_jsx_no_jsx() {
        let result = parse_ts_source("function greet(): string { return 'hello'; }");
        assert!(!result.has_errors);
        let greet = result.symbols.iter().find(|s| s.name == "greet").unwrap();
        assert_eq!(
            greet.kind,
            SymbolKind::Function,
            "non-jsx function should not be component"
        );
    }

    #[test]
    fn test_is_jsx_node_fragment() {
        let result = parse_tsx_source(
            r#"
export function Empty() {
    return <></>;
}
"#,
        );
        assert!(!result.has_errors);
        let empty = result.symbols.iter().find(|s| s.name == "Empty");
        assert!(empty.is_some());
        assert_eq!(empty.unwrap().kind, SymbolKind::Component);
    }

    #[test]
    fn test_arrow_no_jsx_is_function() {
        let result = parse_ts_source(
            r#"
export const compute = (x: number) => {
    return x * 2;
};
"#,
        );
        assert!(!result.has_errors);
        let compute = result.symbols.iter().find(|s| s.name == "compute");
        assert!(compute.is_some());
        assert_eq!(compute.unwrap().kind, SymbolKind::Function);
    }

    #[test]
    fn test_is_ts_builtin_type_exhaustive() {
        for ty in &[
            "string",
            "number",
            "boolean",
            "void",
            "any",
            "unknown",
            "never",
            "null",
            "undefined",
            "object",
            "symbol",
            "bigint",
            "Array",
            "Promise",
            "Map",
            "Set",
            "Record",
            "Partial",
            "Required",
            "Readonly",
            "Pick",
            "Omit",
            "Exclude",
            "Extract",
            "ReturnType",
            "Parameters",
        ] {
            assert!(is_ts_builtin_type(ty), "'{}' should be builtin", ty);
        }
    }

    #[test]
    fn test_import_side_effect_only() {
        let result = parse_ts_source("import './polyfills';");
        assert!(!result.has_errors);
        assert_eq!(result.imports.len(), 1);
        assert_eq!(result.imports[0].path, "./polyfills");
        assert!(
            result.imports[0].imported_symbols.is_empty(),
            "side-effect import should have no imported symbols"
        );
    }

    #[test]
    fn test_export_statement_wraps_children() {
        let result = parse_ts_source("export function exported() { return 1; }");
        assert!(!result.has_errors);
        let sym = result.symbols.iter().find(|s| s.name == "exported");
        assert!(sym.is_some(), "exported function should be found");
        assert_eq!(sym.unwrap().kind, SymbolKind::Function);
    }

    #[test]
    fn test_tsconfig_paths_with_multiple_replacements() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        let lib = tmp.path().join("lib");
        fs::create_dir_all(&src).unwrap();
        fs::create_dir_all(&lib).unwrap();
        fs::write(lib.join("utils.ts"), "export function foo() {}").unwrap();

        fs::write(
            tmp.path().join("tsconfig.json"),
            r#"{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*", "./lib/*"] } } }"#,
        )
        .unwrap();

        let resolver = TypeScriptResolver::new(tmp.path());
        let result = resolver.resolve_import("@/utils", "src/App.tsx", tmp.path());
        assert!(
            result.is_some(),
            "should resolve via second path replacement"
        );
        assert!(result.unwrap().ends_with("utils.ts"));
    }

    #[test]
    fn test_resolve_direct_file_with_extension() {
        let tmp = tempfile::tempdir().unwrap();
        let src = tmp.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("data.json"), r#"{"key": "value"}"#).unwrap();

        let target = src.join("data.json");
        let result = resolve_ts_file(&target);
        assert!(result.is_some(), "direct file should resolve");
    }

    #[test]
    fn test_resolve_ts_file_nonexistent() {
        let tmp = tempfile::tempdir().unwrap();
        let target = tmp.path().join("nonexistent");
        let result = resolve_ts_file(&target);
        assert!(result.is_none(), "nonexistent path should return None");
    }

    // ── Additional edge case tests ──

    #[test]
    fn test_parse_getter_setter() {
        let result = parse_ts_source(
            r#"
class Config {
    private _name: string = '';
    get name(): string { return this._name; }
    set name(v: string) { this._name = v; }
}
"#,
        );
        assert!(!result.has_errors);
        let methods: Vec<&str> = result
            .symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Method)
            .map(|s| s.name.as_str())
            .collect();
        // getter and setter should both be extracted as methods
        assert!(
            methods.iter().filter(|&&n| n == "name").count() >= 2,
            "getter and setter should both appear: {:?}",
            methods
        );
    }

    #[test]
    fn test_parse_static_method() {
        let result = parse_ts_source(
            r#"
class Factory {
    static create(): Factory { return new Factory(); }
    build(): void {}
}
"#,
        );
        assert!(!result.has_errors);
        let methods: Vec<&str> = result
            .symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Method)
            .map(|s| s.name.as_str())
            .collect();
        assert!(methods.contains(&"create"), "static method: {:?}", methods);
        assert!(methods.contains(&"build"), "instance method: {:?}", methods);
    }

    #[test]
    fn test_parse_abstract_class_not_extracted() {
        // abstract class uses "abstract_class_declaration" node kind in tree-sitter,
        // which is not matched by the parser (only "class_declaration" is).
        // This test documents the current behavior.
        let result = parse_ts_source(
            r#"
abstract class BaseHandler {
    abstract handle(): void;
    log(): void { console.log('log'); }
}
"#,
        );
        assert!(!result.has_errors);
        let class = result.symbols.iter().find(|s| s.name == "BaseHandler");
        assert!(
            class.is_none(),
            "abstract class is not extracted by current parser: {:?}",
            result.symbols
        );
    }

    #[test]
    fn test_parse_declare_function() {
        // declare statements should be parsed without errors
        let result = parse_ts_source("declare function readFile(path: string): string;");
        assert!(!result.has_errors);
    }

    #[test]
    fn test_parse_declare_module() {
        let result = parse_ts_source(
            r#"
declare module 'my-lib' {
    export function init(): void;
}
"#,
        );
        assert!(!result.has_errors);
    }

    #[test]
    fn test_type_ref_union_types() {
        let result = parse_ts_source(
            r#"
interface Config { port: number; }
interface Options { timeout: number; }
function create(input: Config | Options): void {}
"#,
        );
        let ref_types: Vec<&str> = result
            .type_refs
            .iter()
            .map(|r| r.to_type.as_str())
            .collect();
        assert!(
            ref_types.contains(&"Config"),
            "union type Config: {:?}",
            ref_types
        );
        assert!(
            ref_types.contains(&"Options"),
            "union type Options: {:?}",
            ref_types
        );
    }

    #[test]
    fn test_type_ref_intersection_types() {
        let result = parse_ts_source(
            r#"
interface Base { id: number; }
interface Named { name: string; }
function process(item: Base & Named): void {}
"#,
        );
        let ref_types: Vec<&str> = result
            .type_refs
            .iter()
            .map(|r| r.to_type.as_str())
            .collect();
        assert!(
            ref_types.contains(&"Base"),
            "intersection type: {:?}",
            ref_types
        );
        assert!(
            ref_types.contains(&"Named"),
            "intersection type: {:?}",
            ref_types
        );
    }

    #[test]
    fn test_export_default_function() {
        let result = parse_ts_source("export default function handler() { return 1; }");
        assert!(!result.has_errors);
        let sym = result.symbols.iter().find(|s| s.name == "handler");
        assert!(sym.is_some(), "export default function should be extracted");
    }

    #[test]
    fn test_multiple_classes_with_methods() {
        let result = parse_ts_source(
            r#"
class A {
    methodA(): void {}
}
class B {
    methodB(): void {}
}
"#,
        );
        assert!(!result.has_errors);
        let names: Vec<&str> = result.symbols.iter().map(|s| s.name.as_str()).collect();
        assert!(names.contains(&"A"));
        assert!(names.contains(&"methodA"));
        assert!(names.contains(&"B"));
        assert!(names.contains(&"methodB"));
        // methodA should be child of A, not B
        let a_idx = result.symbols.iter().position(|s| s.name == "A").unwrap();
        let method_a = result.symbols.iter().find(|s| s.name == "methodA").unwrap();
        assert_eq!(method_a.parent_index, Some(a_idx));
    }

    #[test]
    fn test_conditional_jsx_return() {
        let result = parse_tsx_source(
            r#"
export function Toggle({ on }: { on: boolean }) {
    if (on) {
        return <span>On</span>;
    }
    return <span>Off</span>;
}
"#,
        );
        assert!(!result.has_errors);
        let toggle = result.symbols.iter().find(|s| s.name == "Toggle").unwrap();
        assert_eq!(toggle.kind, SymbolKind::Component);
    }

    #[test]
    fn test_arrow_component_with_block_body() {
        let result = parse_tsx_source(
            r#"
export const Card = ({ title }: { title: string }) => {
    const formatted = title.toUpperCase();
    return <div>{formatted}</div>;
};
"#,
        );
        assert!(!result.has_errors);
        let card = result.symbols.iter().find(|s| s.name == "Card").unwrap();
        assert_eq!(card.kind, SymbolKind::Component);
    }

    #[test]
    fn test_import_type_only() {
        let result = parse_ts_source("import type { Config, Options } from './types';");
        assert!(!result.has_errors);
        assert_eq!(result.imports.len(), 1);
        assert_eq!(result.imports[0].path, "./types");
        assert!(result.imports[0]
            .imported_symbols
            .contains(&"Config".to_string()));
    }

    #[test]
    fn test_re_export() {
        let result = parse_ts_source("export { default as Button } from './Button';");
        assert!(!result.has_errors);
    }

    #[test]
    fn test_language_parser_trait_name() {
        let parser = TypeScriptParser;
        assert_eq!(parser.language_name(), "typescript");
    }

    #[test]
    fn test_language_parser_tsx_vs_ts() {
        let parser = TypeScriptParser;
        // .ts file should not parse JSX
        let ts_result = parser.parse("function foo() { return 1; }", &PathBuf::from("test.ts"));
        assert!(!ts_result.has_errors);

        // .tsx file should parse JSX
        let tsx_result = parser.parse(
            "export function App() { return <div/>; }",
            &PathBuf::from("test.tsx"),
        );
        assert!(!tsx_result.has_errors);
        let app = tsx_result.symbols.iter().find(|s| s.name == "App").unwrap();
        assert_eq!(app.kind, SymbolKind::Component);
    }

    #[test]
    fn test_match_tsconfig_pattern_no_wildcard() {
        // Exact match without wildcard
        assert_eq!(
            match_tsconfig_pattern("lodash", "lodash"),
            Some(String::new())
        );
        assert_eq!(match_tsconfig_pattern("lodash", "lodash/fp"), None);
        assert_eq!(match_tsconfig_pattern("lodash", "underscore"), None);
    }

    #[test]
    fn test_calls_in_arrow_function() {
        let result = parse_ts_source(
            r#"
export const init = () => {
    setup();
    configure();
};
"#,
        );
        let callee_names: Vec<&str> = result
            .calls
            .iter()
            .map(|c| c.callee_name.as_str())
            .collect();
        assert!(callee_names.contains(&"setup"), "calls: {:?}", result.calls);
        assert!(
            callee_names.contains(&"configure"),
            "calls: {:?}",
            result.calls
        );
    }
}
