//! Metric extraction from AST and CFG
//!
//! Global invariants enforced:
//! - Formatting, comments, and whitespace must not affect results
//! - Deterministic metric calculation

use crate::ast::FunctionNode;
use crate::cfg::Cfg;
use swc_ecma_ast::*;
use swc_ecma_visit::{Visit, VisitWith};

/// Raw metrics for a function
#[derive(Debug, Clone)]
pub struct RawMetrics {
    pub cc: usize,
    pub nd: usize,
    pub fo: usize,
    pub ns: usize,
    pub loc: usize,
    /// Callee names extracted from AST (for tree-sitter languages).
    /// Empty for ECMAScript/Rust (which retain regex-based call graph extraction).
    pub callee_names: Vec<String>,
}

/// Calculate lines of code (LOC) from source text
/// Counts physical lines (including blank lines and comments)
fn calculate_loc(source: &str) -> usize {
    if source.is_empty() {
        return 0;
    }
    // Count newlines + 1 for the last line (which may not end with \n)
    source.lines().count()
}

/// Calculate LOC from tree-sitter node
fn calculate_loc_from_node(node: &tree_sitter::Node) -> usize {
    let start_row = node.start_position().row;
    let end_row = node.end_position().row;
    // Rows are 0-indexed, so difference + 1 gives line count
    end_row.saturating_sub(start_row) + 1
}

/// Extract all metrics for a function
pub fn extract_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
    use crate::language::FunctionBody;

    match &function.body {
        FunctionBody::ECMAScript(body) => {
            // Calculate LOC from span (end_line - start_line + 1)
            let loc = function
                .span
                .end_line
                .saturating_sub(function.span.start_line)
                + 1;

            let callee_names = ecmascript_extract_callees(body);
            RawMetrics {
                cc: cyclomatic_complexity(cfg, body),
                nd: nesting_depth(body),
                fo: callee_names.len(),
                ns: non_structured_exits(body),
                loc: loc as usize,
                callee_names,
            }
        }
        FunctionBody::Go { .. } => {
            // Extract Go-specific metrics from tree-sitter AST
            extract_go_metrics(function, cfg)
        }
        FunctionBody::Java { .. } => {
            // Extract Java-specific metrics from tree-sitter AST
            extract_java_metrics(function, cfg)
        }
        FunctionBody::Python { .. } => {
            // Extract Python-specific metrics from tree-sitter AST
            extract_python_metrics(function, cfg)
        }
        FunctionBody::Rust { .. } => {
            // Extract Rust-specific metrics from syn AST
            extract_rust_metrics(function, cfg)
        }
        FunctionBody::CSharp { .. } => extract_csharp_metrics(function, cfg),
        FunctionBody::C { .. } => extract_c_metrics(function, cfg),
    }
}

/// Calculate cyclomatic complexity from CFG alone
/// Used for languages where we don't yet have full AST metrics
fn calculate_cc_from_cfg(cfg: &Cfg) -> usize {
    // Base formula: CC = E - N + 2
    if cfg.edge_count() > 0 && cfg.node_count() > 2 {
        let e = cfg.edge_count();
        let n = cfg.node_count() - 2; // Exclude entry and exit
        if n > 0 {
            e.saturating_sub(n).saturating_add(2)
        } else {
            1
        }
    } else {
        1
    }
}

/// Calculate Cyclomatic Complexity: CC = E - N + 2
///
/// Additional increments:
/// - Boolean short-circuit operators (&&, ||)
/// - Each switch case
/// - Each catch clause
fn cyclomatic_complexity(cfg: &Cfg, body: &BlockStmt) -> usize {
    // Base formula: CC = E - N + 2
    let base_cc = if cfg.edge_count() > 0 && cfg.node_count() > 2 {
        // Exclude entry and exit nodes for calculation
        // E = number of edges
        // N = number of nodes (excluding entry/exit which are structural)
        let e = cfg.edge_count();
        let n = cfg.node_count() - 2; // Exclude entry and exit
        if n > 0 {
            e.saturating_sub(n).saturating_add(2)
        } else {
            1 // Minimum CC for any function
        }
    } else {
        1 // Empty function has CC = 1
    };

    // Increment for boolean short-circuit operators
    let mut short_circuit_count = 0;
    let mut visitor = ShortCircuitVisitor {
        count: &mut short_circuit_count,
    };
    body.visit_with(&mut visitor);

    // Increment for switch cases
    let switch_case_count = count_switch_cases(body);

    // Increment for catch clauses
    let catch_count = count_catch_clauses(body);

    base_cc + short_circuit_count + switch_case_count + catch_count
}

/// Visitor to count boolean short-circuit operators
struct ShortCircuitVisitor<'a> {
    count: &'a mut usize,
}

impl Visit for ShortCircuitVisitor<'_> {
    fn visit_bin_expr(&mut self, bin_expr: &BinExpr) {
        match bin_expr.op {
            BinaryOp::LogicalAnd | BinaryOp::LogicalOr => {
                *self.count += 1;
            }
            _ => {}
        }
        bin_expr.visit_children_with(self);
    }
}

/// Count switch cases in the AST
fn count_switch_cases(body: &BlockStmt) -> usize {
    let mut count = 0;
    let mut visitor = SwitchCaseCounter { count: &mut count };
    body.visit_with(&mut visitor);
    count
}

struct SwitchCaseCounter<'a> {
    count: &'a mut usize,
}

impl Visit for SwitchCaseCounter<'_> {
    fn visit_switch_stmt(&mut self, switch_stmt: &SwitchStmt) {
        // Count each case in the switch
        *self.count += switch_stmt.cases.len();
        switch_stmt.visit_children_with(self);
    }
}

/// Count catch clauses in the AST
fn count_catch_clauses(body: &BlockStmt) -> usize {
    let mut count = 0;
    let mut visitor = CatchCounter { count: &mut count };
    body.visit_with(&mut visitor);
    count
}

struct CatchCounter<'a> {
    count: &'a mut usize,
}

impl Visit for CatchCounter<'_> {
    fn visit_try_stmt(&mut self, try_stmt: &TryStmt) {
        // Count catch clause if present
        if try_stmt.handler.is_some() {
            *self.count += 1;
        }
        try_stmt.visit_children_with(self);
    }
}

/// Calculate Nesting Depth (ND)
///
/// Walk AST and count maximum depth of control constructs:
/// - if, loop, switch, try
fn nesting_depth(body: &BlockStmt) -> usize {
    let mut visitor = NestingDepthVisitor {
        max_depth: 0,
        current_depth: 0,
    };
    body.visit_with(&mut visitor);
    visitor.max_depth
}

struct NestingDepthVisitor {
    max_depth: usize,
    current_depth: usize,
}

macro_rules! impl_nesting_visitor {
    ($($method:ident, $ty:ty, $node:ident);* $(;)?) => {
        $(
            fn $method(&mut self, $node: &$ty) {
                self.current_depth += 1;
                if self.current_depth > self.max_depth {
                    self.max_depth = self.current_depth;
                }
                $node.visit_children_with(self);
                self.current_depth -= 1;
            }
        )*
    };
}

impl Visit for NestingDepthVisitor {
    impl_nesting_visitor!(
        visit_if_stmt,     IfStmt,     if_stmt;
        visit_while_stmt,  WhileStmt,  while_stmt;
        visit_do_while_stmt, DoWhileStmt, do_while_stmt;
        visit_for_stmt,    ForStmt,    for_stmt;
        visit_for_in_stmt, ForInStmt,  for_in_stmt;
        visit_for_of_stmt, ForOfStmt,  for_of_stmt;
        visit_switch_stmt, SwitchStmt, switch_stmt;
        visit_try_stmt,    TryStmt,    try_stmt;
    );
}

/// Calculate Fan-Out (FO)
///
/// Count number of unique functions called by this function
fn ecmascript_extract_callees(body: &BlockStmt) -> Vec<String> {
    let mut visitor = FanOutVisitor {
        calls: std::collections::HashSet::new(),
    };
    body.visit_with(&mut visitor);
    let mut names: Vec<String> = visitor.calls.into_iter().collect();
    names.sort();
    names
}

struct FanOutVisitor {
    calls: std::collections::HashSet<String>,
}

impl Visit for FanOutVisitor {
    fn visit_call_expr(&mut self, call_expr: &CallExpr) {
        // Extract callee representation
        let callee_str = callee_to_string(&call_expr.callee);
        if !callee_str.is_empty() && callee_str != "<computed>" {
            self.calls.insert(callee_str);
        }

        // Continue visiting children (to catch chained calls)
        call_expr.visit_children_with(self);
    }
}

/// Convert a callee expression to string representation
fn callee_to_string(callee: &Callee) -> String {
    match callee {
        Callee::Expr(expr) => expr_to_callee_string(expr),
        Callee::Super(_) => "super".to_string(),
        Callee::Import(_) => "<computed>".to_string(),
    }
}

/// Extract string representation from expression for callee
///
/// For chained calls like foo().bar().baz(), we extract:
/// - foo (when visiting the inner CallExpr)
/// - foo().bar (when visiting the middle CallExpr with MemberExpr callee)
/// - foo().bar().baz (when visiting the outer CallExpr with MemberExpr callee)
fn expr_to_callee_string(expr: &Expr) -> String {
    match expr {
        Expr::Ident(ident) => ident.sym.to_string(),
        Expr::Member(member) => {
            // For member expressions like obj.method, represent as obj.method
            // The obj might be a call (for chained calls)
            let obj_str = match &*member.obj {
                Expr::Ident(id) => id.sym.to_string(),
                Expr::Call(call) => {
                    // Chained call - extract the callee of the inner call
                    // This gives us the full chain like "foo().bar" when processing "foo().bar().baz"
                    match &call.callee {
                        Callee::Expr(callee_expr) => expr_to_callee_string(callee_expr),
                        _ => "<computed>".to_string(),
                    }
                }
                Expr::Member(member_obj) => {
                    // Nested member expression - recursively build the chain
                    expr_to_callee_string(&member_obj.obj)
                }
                _ => "<computed>".to_string(),
            };

            let prop_str = match &member.prop {
                MemberProp::Ident(id) => id.sym.to_string(),
                MemberProp::PrivateName(name) => name.name.to_string(),
                MemberProp::Computed(_) => "<computed>".to_string(),
            };

            if obj_str == "<computed>" || prop_str == "<computed>" {
                "<computed>".to_string()
            } else {
                format!("{}.{}", obj_str, prop_str)
            }
        }
        Expr::Call(_) => "<computed>".to_string(), // Should not happen - CallExpr callee should be MemberExpr or Ident
        _ => "<computed>".to_string(),
    }
}

/// Calculate Non-Structured Exits (NS)
///
/// Count:
/// - Early return statements (excluding final tail return)
/// - Break statements
/// - Continue statements
/// - Throw statements
fn non_structured_exits(body: &BlockStmt) -> usize {
    let mut visitor = NonStructuredExitVisitor {
        count: 0,
        return_count: 0,
    };
    body.visit_with(&mut visitor);

    // Check if last statement is a return (final tail return)
    let has_final_return = body
        .stmts
        .last()
        .map(|s| matches!(s, Stmt::Return(_)))
        .unwrap_or(false);

    // Exclude final return if present
    if has_final_return && visitor.return_count > 0 {
        visitor.count -= 1;
    }

    visitor.count
}

struct NonStructuredExitVisitor {
    count: usize,
    return_count: usize,
}

impl Visit for NonStructuredExitVisitor {
    fn visit_return_stmt(&mut self, _return_stmt: &ReturnStmt) {
        self.count += 1;
        self.return_count += 1;
    }

    fn visit_break_stmt(&mut self, _break_stmt: &BreakStmt) {
        self.count += 1;
    }

    fn visit_continue_stmt(&mut self, _continue_stmt: &ContinueStmt) {
        self.count += 1;
    }

    fn visit_throw_stmt(&mut self, _throw_stmt: &ThrowStmt) {
        self.count += 1;
    }
}

// ============================================================================
// Shared Tree-Sitter Metric Utilities
//
// Generic tree-sitter traversals parameterised by language-specific node-kind
// sets. Per-language code is reduced to providing the right kind lists.
// ============================================================================

/// Find the first immediate child of `node` whose kind matches `kind`.
fn ts_find_child_by_kind<'a>(
    node: tree_sitter::Node<'a>,
    kind: &str,
) -> Option<tree_sitter::Node<'a>> {
    let mut cursor = node.walk();
    let result = node
        .children(&mut cursor)
        .find(|child| child.kind() == kind);
    result
}

/// Find the first descendant that belongs to `func_kinds` at `start_byte`.
fn ts_find_function_by_start<'a>(
    root: tree_sitter::Node<'a>,
    start_byte: usize,
    func_kinds: &[&str],
) -> Option<tree_sitter::Node<'a>> {
    if func_kinds.contains(&root.kind()) && root.start_byte() == start_byte {
        return Some(root);
    }
    let mut cursor = root.walk();
    for child in root.children(&mut cursor) {
        if let Some(found) = ts_find_function_by_start(child, start_byte, func_kinds) {
            return Some(found);
        }
    }
    None
}

/// Calculate maximum nesting depth for the given control-structure node kinds.
fn ts_nesting_depth(body_node: &tree_sitter::Node, nesting_kinds: &[&str]) -> usize {
    fn recurse(node: tree_sitter::Node, kinds: &[&str], current: usize, max: &mut usize) {
        let next = if kinds.contains(&node.kind()) {
            let d = current + 1;
            if d > *max {
                *max = d;
            }
            d
        } else {
            current
        };
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            recurse(child, kinds, next, max);
        }
    }
    let mut max_depth = 0;
    recurse(*body_node, nesting_kinds, 0, &mut max_depth);
    max_depth
}

/// Count exits whose node kind appears in `exit_kinds`.
fn ts_non_structured_exits(body_node: &tree_sitter::Node, exit_kinds: &[&str]) -> usize {
    fn recurse(node: tree_sitter::Node, kinds: &[&str], count: &mut usize) {
        if kinds.contains(&node.kind()) {
            *count += 1;
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            recurse(child, kinds, count);
        }
    }
    let mut count = 0;
    recurse(*body_node, exit_kinds, &mut count);
    count
}

/// Parse `source` with `language`, locate the function starting at `start_byte`,
/// find the first matching body child, and call `f(func_node, body_node)`.
/// Returns `None` if the function or body cannot be found.
fn ts_with_function_body<R>(
    source: &str,
    language: tree_sitter::Language,
    start_byte: usize,
    func_kinds: &[&str],
    body_kinds: &[&str],
    f: impl FnOnce(tree_sitter::Node, tree_sitter::Node) -> R,
) -> Option<R> {
    let mut parser = tree_sitter::Parser::new();
    parser.set_language(&language).ok()?;
    let tree = parser.parse(source, None)?;
    let root = tree.root_node();
    let func_node = ts_find_function_by_start(root, start_byte, func_kinds)?;
    for kind in body_kinds {
        if let Some(body_node) = ts_find_child_by_kind(func_node, kind) {
            return Some(f(func_node, body_node));
        }
    }
    None
}

// ============================================================================
// Go Metrics Implementation
// ============================================================================

/// Extract metrics for Go functions using tree-sitter
fn extract_go_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
    let (_body_node_id, source) = function.body.as_go();
    ts_with_function_body(
        source,
        tree_sitter_go::LANGUAGE.into(),
        function.span.start,
        &["function_declaration", "method_declaration"],
        &["block"],
        |func_node, body_node| {
            let callee_names = go_extract_callees(&body_node, source);
            RawMetrics {
                cc: calculate_cc_from_cfg(cfg) + go_count_cc_extras(&body_node, source),
                nd: ts_nesting_depth(
                    &body_node,
                    &[
                        "if_statement",
                        "for_statement",
                        "switch_statement",
                        "expression_switch_statement",
                        "type_switch_statement",
                        "select_statement",
                    ],
                ),
                fo: callee_names.len(),
                ns: go_non_structured_exits(&body_node, source),
                loc: calculate_loc_from_node(&func_node),
                callee_names,
            }
        },
    )
    .unwrap_or(RawMetrics {
        cc: 1,
        nd: 0,
        fo: 0,
        ns: 0,
        loc: 0,
        callee_names: vec![],
    })
}

/// Extract callee names from a Go function body (function calls + go statements).
/// Returns the unique set of callee name strings (used for both fo metric and call graph).
fn go_extract_callees(body_node: &tree_sitter::Node, source: &str) -> Vec<String> {
    use std::collections::HashSet;

    fn collect(node: tree_sitter::Node, source: &str, calls: &mut HashSet<String>) {
        match node.kind() {
            "call_expression" => {
                if let Some(func_node) = ts_find_child_by_kind(node, "identifier")
                    .or_else(|| ts_find_child_by_kind(node, "selector_expression"))
                {
                    let func_text = &source[func_node.start_byte()..func_node.end_byte()];
                    calls.insert(func_text.to_string());
                }
            }
            "go_statement" => {
                // Go statements spawn goroutines, count as fan-out
                calls.insert(format!("<go@{}>", node.start_byte()));
            }
            _ => {}
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            collect(child, source, calls);
        }
    }

    let mut calls = HashSet::new();
    collect(*body_node, source, &mut calls);
    let mut result: Vec<String> = calls.into_iter().collect();
    result.sort();
    result
}

/// Calculate non-structured exits for Go function
fn go_non_structured_exits(body_node: &tree_sitter::Node, source: &str) -> usize {
    fn count_exits(node: tree_sitter::Node, source: &str, count: &mut usize) {
        match node.kind() {
            "return_statement" => *count += 1,
            "defer_statement" => *count += 1,
            "expression_statement" => {
                if let Some(call) = ts_find_child_by_kind(node, "call_expression") {
                    // Bare panic() call
                    if let Some(ident) = ts_find_child_by_kind(call, "identifier") {
                        let name = &source[ident.start_byte()..ident.end_byte()];
                        if name == "panic" {
                            *count += 1;
                        }
                    }
                    // os.Exit / log.Fatal* via selector_expression
                    else if let Some(sel) = ts_find_child_by_kind(call, "selector_expression") {
                        if let Some(field) = ts_find_child_by_kind(sel, "field_identifier") {
                            let field_name = &source[field.start_byte()..field.end_byte()];
                            if matches!(field_name, "Exit" | "Fatal" | "Fatalf" | "Fatalln") {
                                *count += 1;
                            }
                        }
                    }
                }
            }
            _ => {}
        }

        // Recurse into children
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            count_exits(child, source, count);
        }
    }

    let mut count = 0;
    count_exits(*body_node, source, &mut count);

    // Subtract 1 if the last statement is a return (final tail return)
    // This is an approximation - would need more sophisticated AST analysis
    if count > 0 {
        let mut cursor = body_node.walk();
        if let Some(last_child) = body_node.children(&mut cursor).last() {
            if last_child.kind() == "return_statement" {
                count = count.saturating_sub(1);
            }
        }
    }

    count
}

/// Count additional cyclomatic complexity contributors for Go
fn go_count_cc_extras(body_node: &tree_sitter::Node, _source: &str) -> usize {
    fn count_extras(node: tree_sitter::Node, count: &mut usize) {
        match node.kind() {
            // Count switch/select cases
            "expression_case" | "default_case" | "communication_case" | "type_case" => {
                *count += 1;
            }
            // Count boolean operators — check the operator child directly to
            // avoid false positives from nested logical operators in sub-expressions
            "binary_expression" => {
                let mut cursor = node.walk();
                for child in node.children(&mut cursor) {
                    if child.kind() == "&&" || child.kind() == "||" {
                        *count += 1;
                        break;
                    }
                }
            }
            _ => {}
        }

        // Recurse into children
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            count_extras(child, count);
        }
    }

    let mut count = 0;
    count_extras(*body_node, &mut count);
    count
}

// Note: Go metrics tests are integrated with cfg_builder tests

// ============================================================================
// Java Metrics Implementation
// ============================================================================

/// Extract metrics for Java functions using tree-sitter
fn extract_java_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
    let (_body_node_id, source) = function.body.as_java();
    ts_with_function_body(
        source,
        tree_sitter_java::LANGUAGE.into(),
        function.span.start,
        &["method_declaration", "constructor_declaration"],
        &["block", "constructor_body"],
        |func_node, body_node| {
            let callee_names = java_extract_callees(&body_node, source);
            RawMetrics {
                cc: calculate_cc_from_cfg(cfg) + java_count_cc_extras(&body_node, source),
                nd: ts_nesting_depth(
                    &body_node,
                    &[
                        "if_statement",
                        "while_statement",
                        "do_statement",
                        "for_statement",
                        "enhanced_for_statement",
                        "switch_statement",
                        "switch_expression",
                        "try_statement",
                        "synchronized_statement",
                    ],
                ),
                fo: callee_names.len(),
                ns: ts_non_structured_exits(
                    &body_node,
                    &[
                        "return_statement",
                        "throw_statement",
                        "break_statement",
                        "continue_statement",
                    ],
                ),
                loc: calculate_loc_from_node(&func_node),
                callee_names,
            }
        },
    )
    .unwrap_or(RawMetrics {
        cc: 1,
        nd: 0,
        fo: 0,
        ns: 0,
        loc: 0,
        callee_names: vec![],
    })
}

/// Extract callee names from a Java function body.
/// Returns the unique set of method invocation strings.
fn java_extract_callees(body_node: &tree_sitter::Node, source: &str) -> Vec<String> {
    fn collect(
        node: tree_sitter::Node,
        source: &str,
        calls: &mut std::collections::HashSet<String>,
    ) {
        if node.kind() == "method_invocation" {
            let method_text = &source[node.start_byte()..node.end_byte()];
            calls.insert(method_text.to_string());
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            collect(child, source, calls);
        }
    }

    let mut calls = std::collections::HashSet::new();
    collect(*body_node, source, &mut calls);
    let mut result: Vec<String> = calls.into_iter().collect();
    result.sort();
    result
}

/// Count additional CC contributors in Java
/// (ternary expressions, boolean operators)
fn java_count_cc_extras(body_node: &tree_sitter::Node, _source: &str) -> usize {
    fn count_extras(node: tree_sitter::Node, count: &mut usize) {
        match node.kind() {
            // Ternary expressions (conditional_expression) add to CC
            "ternary_expression" => {
                *count += 1;
            }
            // Binary expressions with && or || add to CC — check the operator
            // child directly to avoid false positives from nested sub-expressions
            "binary_expression" => {
                let mut cursor = node.walk();
                for child in node.children(&mut cursor) {
                    if child.kind() == "&&" || child.kind() == "||" {
                        *count += 1;
                        break;
                    }
                }
            }
            _ => {}
        }

        // Recursively check children
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            count_extras(child, count);
        }
    }

    let mut count = 0;
    count_extras(*body_node, &mut count);
    count
}

// ============================================================================
// Python Metrics Implementation
// ============================================================================

/// Extract metrics for Python functions using tree-sitter
fn extract_python_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
    let (_body_node_id, source) = function.body.as_python();
    ts_with_function_body(
        source,
        tree_sitter_python::LANGUAGE.into(),
        function.span.start,
        &["function_definition", "async_function_definition"],
        &["block"],
        |func_node, body_node| {
            let callee_names = python_extract_callees(&body_node, source);
            RawMetrics {
                cc: calculate_cc_from_cfg(cfg) + python_count_cc_extras(&body_node, source),
                nd: ts_nesting_depth(
                    &body_node,
                    &[
                        "if_statement",
                        "while_statement",
                        "for_statement",
                        "try_statement",
                        "with_statement",
                        "match_statement",
                    ],
                ),
                fo: callee_names.len(),
                ns: ts_non_structured_exits(
                    &body_node,
                    &[
                        "return_statement",
                        "raise_statement",
                        "break_statement",
                        "continue_statement",
                    ],
                ),
                loc: calculate_loc_from_node(&func_node),
                callee_names,
            }
        },
    )
    .unwrap_or(RawMetrics {
        cc: 1,
        nd: 0,
        fo: 0,
        ns: 0,
        loc: 0,
        callee_names: vec![],
    })
}

/// Extract callee names from a Python function body.
/// Returns the unique set of call target strings.
fn python_extract_callees(body_node: &tree_sitter::Node, source: &str) -> Vec<String> {
    fn collect(
        node: tree_sitter::Node,
        source: &str,
        calls: &mut std::collections::HashSet<String>,
    ) {
        if node.kind() == "call" {
            let mut cursor = node.walk();
            if let Some(func_node) = node.children(&mut cursor).next() {
                let func_text = &source[func_node.start_byte()..func_node.end_byte()];
                calls.insert(func_text.to_string());
            };
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            collect(child, source, calls);
        }
    }

    let mut calls = std::collections::HashSet::new();
    collect(*body_node, source, &mut calls);
    let mut result: Vec<String> = calls.into_iter().collect();
    result.sort();
    result
}

/// Count additional CC contributors in Python
/// (comprehensions with if-filters, boolean operators, ternary expressions)
fn python_count_cc_extras(body_node: &tree_sitter::Node, _source: &str) -> usize {
    fn count_extras(node: tree_sitter::Node, count: &mut usize) {
        match node.kind() {
            // Boolean operators (and, or) add to CC
            "boolean_operator" => {
                *count += 1;
            }
            // Ternary expressions add to CC
            "conditional_expression" => {
                *count += 1;
            }
            // Comprehensions with if-filters add to CC
            "list_comprehension"
            | "dictionary_comprehension"
            | "set_comprehension"
            | "generator_expression" => {
                // Check if it has an if_clause child
                let mut cursor = node.walk();
                for child in node.children(&mut cursor) {
                    if child.kind() == "if_clause" {
                        *count += 1;
                        break;
                    }
                }
            }
            _ => {}
        }

        // Recursively check children
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            count_extras(child, count);
        }
    }

    let mut count = 0;
    count_extras(*body_node, &mut count);
    count
}

// Note: Python metrics tests are integrated with cfg_builder tests

// ============================================================================
// C# Metrics Implementation
// ============================================================================

/// Extract metrics for C# functions using tree-sitter
fn extract_csharp_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
    let (_body_node_id, source) = function.body.as_csharp();
    ts_with_function_body(
        source,
        tree_sitter_c_sharp::LANGUAGE.into(),
        function.span.start,
        &[
            "method_declaration",
            "constructor_declaration",
            "local_function_statement",
            "operator_declaration",
            "conversion_operator_declaration",
        ],
        &["block"],
        |func_node, body_node| {
            let callee_names = csharp_extract_callees(&body_node, source);
            RawMetrics {
                cc: calculate_cc_from_cfg(cfg) + csharp_count_cc_extras(&body_node, source),
                nd: ts_nesting_depth(
                    &body_node,
                    &[
                        "if_statement",
                        "while_statement",
                        "do_statement",
                        "for_statement",
                        "foreach_statement",
                        "switch_statement",
                        "try_statement",
                    ],
                ),
                fo: callee_names.len(),
                ns: ts_non_structured_exits(
                    &body_node,
                    &[
                        "return_statement",
                        "throw_statement",
                        "break_statement",
                        "continue_statement",
                    ],
                ),
                loc: calculate_loc_from_node(&func_node),
                callee_names,
            }
        },
    )
    .unwrap_or(RawMetrics {
        cc: 1,
        nd: 0,
        fo: 0,
        ns: 0,
        loc: 0,
        callee_names: vec![],
    })
}

// ============================================================================
// C Metrics Implementation
// ============================================================================

fn extract_c_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
    let (_body_node_id, source) = function.body.as_c();
    ts_with_function_body(
        source,
        tree_sitter_c::LANGUAGE.into(),
        function.span.start,
        &["function_definition"],
        &["compound_statement"],
        |func_node, body_node| {
            let callee_names = c_extract_callees(&body_node, source);
            RawMetrics {
                cc: calculate_cc_from_cfg(cfg) + c_count_cc_extras(&body_node),
                nd: ts_nesting_depth(
                    &body_node,
                    &[
                        "if_statement",
                        "while_statement",
                        "do_statement",
                        "for_statement",
                        "switch_statement",
                    ],
                ),
                fo: callee_names.len(),
                ns: ts_non_structured_exits(
                    &body_node,
                    &[
                        "return_statement",
                        "break_statement",
                        "continue_statement",
                        "goto_statement",
                    ],
                ),
                loc: calculate_loc_from_node(&func_node),
                callee_names,
            }
        },
    )
    .unwrap_or(RawMetrics {
        cc: 1,
        nd: 0,
        fo: 0,
        ns: 0,
        loc: 0,
        callee_names: vec![],
    })
}

/// Extract callee names from a C function body (call_expression nodes).
fn c_extract_callees(body_node: &tree_sitter::Node, source: &str) -> Vec<String> {
    fn collect(
        node: tree_sitter::Node,
        source: &str,
        calls: &mut std::collections::HashSet<String>,
    ) {
        if node.kind() == "call_expression" {
            // First child of call_expression is the function identifier
            let child_count = node.child_count();
            if child_count > 0 {
                if let Some(callee) = node.child(0) {
                    let text = &source[callee.start_byte()..callee.end_byte()];
                    calls.insert(text.to_string());
                }
            }
        }
        let mut cursor = node.walk();
        let children: Vec<_> = node.children(&mut cursor).collect();
        for child in children {
            collect(child, source, calls);
        }
    }

    let mut calls = std::collections::HashSet::new();
    collect(*body_node, source, &mut calls);
    let mut result: Vec<String> = calls.into_iter().collect();
    result.sort();
    result
}

/// Count additional CC contributors in C (ternary expressions, boolean short-circuit operators).
fn c_count_cc_extras(body_node: &tree_sitter::Node) -> usize {
    fn count_extras(node: tree_sitter::Node, count: &mut usize) {
        match node.kind() {
            "conditional_expression" => {
                *count += 1;
            }
            "binary_expression" => {
                let mut cursor = node.walk();
                for child in node.children(&mut cursor) {
                    if child.kind() == "&&" || child.kind() == "||" {
                        *count += 1;
                        break;
                    }
                }
            }
            _ => {}
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            count_extras(child, count);
        }
    }
    let mut count = 0;
    count_extras(*body_node, &mut count);
    count
}

/// Extract callee names from a C# function body.
fn csharp_extract_callees(body_node: &tree_sitter::Node, source: &str) -> Vec<String> {
    fn collect(
        node: tree_sitter::Node,
        source: &str,
        calls: &mut std::collections::HashSet<String>,
    ) {
        if node.kind() == "invocation_expression" {
            let call_text = &source[node.start_byte()..node.end_byte()];
            calls.insert(call_text.to_string());
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            collect(child, source, calls);
        }
    }

    let mut calls = std::collections::HashSet::new();
    collect(*body_node, source, &mut calls);
    let mut result: Vec<String> = calls.into_iter().collect();
    result.sort();
    result
}

/// Count additional CC contributors in C# (ternary, boolean operators, null-coalescing)
fn csharp_count_cc_extras(body_node: &tree_sitter::Node, _source: &str) -> usize {
    fn count_extras(node: tree_sitter::Node, count: &mut usize) {
        match node.kind() {
            "conditional_expression" => {
                *count += 1;
            }
            "binary_expression" => {
                let mut cursor = node.walk();
                for child in node.children(&mut cursor) {
                    if child.kind() == "&&" || child.kind() == "||" || child.kind() == "??" {
                        *count += 1;
                        break;
                    }
                }
            }
            _ => {}
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            count_extras(child, count);
        }
    }

    let mut count = 0;
    count_extras(*body_node, &mut count);
    count
}

// ========================================
// Rust Metrics Extraction
// ========================================

/// Extract metrics for a Rust function
fn extract_rust_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
    let source = function.body.as_rust();

    // Parse the function source
    let item_fn: syn::ItemFn = match syn::parse_str(source) {
        Ok(func) => func,
        Err(_) => {
            // Fallback on parse error
            return RawMetrics {
                cc: calculate_cc_from_cfg(cfg),
                nd: 0,
                fo: 0,
                ns: 0,
                loc: 0,
                callee_names: vec![],
            };
        }
    };

    let base_cc = calculate_cc_from_cfg(cfg);
    let extra_cc = rust_count_cc_extras(&item_fn.block);
    let nd = rust_nesting_depth(&item_fn.block);
    let callee_names = rust_extract_callees(&item_fn.block);
    let ns = rust_non_structured_exits(&item_fn.block);

    RawMetrics {
        cc: base_cc + extra_cc,
        nd,
        fo: callee_names.len(),
        ns,
        loc: calculate_loc(source),
        callee_names,
    }
}

/// Calculate nesting depth for Rust function
fn rust_nesting_depth(block: &syn::Block) -> usize {
    use syn::{Expr, Stmt};

    fn calculate_depth(stmts: &[Stmt], current_depth: usize, max_depth: &mut usize) {
        for stmt in stmts {
            match stmt {
                Stmt::Expr(expr, _) => expr_depth(expr, current_depth, max_depth),
                Stmt::Local(local) => {
                    if let Some(init) = &local.init {
                        expr_depth(&init.expr, current_depth, max_depth);
                    }
                }
                _ => {}
            }
        }
    }

    fn expr_depth(expr: &Expr, current_depth: usize, max_depth: &mut usize) {
        let new_depth = match expr {
            Expr::If(_) | Expr::Match(_) | Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => {
                let depth = current_depth + 1;
                if depth > *max_depth {
                    *max_depth = depth;
                }
                depth
            }
            _ => current_depth,
        };

        // Recurse into sub-expressions
        match expr {
            Expr::If(expr_if) => {
                calculate_depth(&expr_if.then_branch.stmts, new_depth, max_depth);
                if let Some((_, else_expr)) = &expr_if.else_branch {
                    expr_depth(else_expr, new_depth, max_depth);
                }
            }
            Expr::Match(expr_match) => {
                for arm in &expr_match.arms {
                    expr_depth(&arm.body, new_depth, max_depth);
                }
            }
            Expr::Loop(expr_loop) => {
                calculate_depth(&expr_loop.body.stmts, new_depth, max_depth);
            }
            Expr::While(expr_while) => {
                calculate_depth(&expr_while.body.stmts, new_depth, max_depth);
            }
            Expr::ForLoop(expr_for) => {
                calculate_depth(&expr_for.body.stmts, new_depth, max_depth);
            }
            Expr::Block(expr_block) => {
                calculate_depth(&expr_block.block.stmts, new_depth, max_depth);
            }
            _ => {}
        }
    }

    let mut max_depth = 0;
    calculate_depth(&block.stmts, 0, &mut max_depth);
    max_depth
}

/// Extract callee names from a Rust function body.
/// Returns the deduplicated, sorted set of called function/method/macro names.
fn rust_extract_callees(block: &syn::Block) -> Vec<String> {
    use std::collections::HashSet;
    use syn::{Expr, ExprCall, ExprMethodCall, Stmt};

    fn count_calls(stmts: &[Stmt], calls: &mut HashSet<String>) {
        for stmt in stmts {
            match stmt {
                Stmt::Expr(expr, _) => expr_calls(expr, calls),
                Stmt::Local(local) => {
                    if let Some(init) = &local.init {
                        expr_calls(&init.expr, calls);
                    }
                }
                Stmt::Macro(stmt_macro) => {
                    // Count macro invocations
                    let macro_name = stmt_macro
                        .mac
                        .path
                        .segments
                        .last()
                        .map(|seg| seg.ident.to_string())
                        .unwrap_or_else(|| "macro".to_string());
                    calls.insert(macro_name);
                }
                _ => {}
            }
        }
    }

    fn expr_calls(expr: &Expr, calls: &mut HashSet<String>) {
        match expr {
            Expr::Call(ExprCall { func, .. }) => {
                // Extract function name from path
                if let Expr::Path(expr_path) = &**func {
                    let func_name = expr_path
                        .path
                        .segments
                        .last()
                        .map(|seg| seg.ident.to_string())
                        .unwrap_or_else(|| "fn".to_string());
                    calls.insert(func_name);
                }
            }
            Expr::MethodCall(ExprMethodCall { method, .. }) => {
                calls.insert(method.to_string());
            }
            Expr::Macro(expr_macro) => {
                let macro_name = expr_macro
                    .mac
                    .path
                    .segments
                    .last()
                    .map(|seg| seg.ident.to_string())
                    .unwrap_or_else(|| "macro".to_string());
                calls.insert(macro_name);
            }
            Expr::If(expr_if) => {
                expr_calls(&expr_if.cond, calls);
                count_calls(&expr_if.then_branch.stmts, calls);
                if let Some((_, else_expr)) = &expr_if.else_branch {
                    expr_calls(else_expr, calls);
                }
            }
            Expr::Match(expr_match) => {
                expr_calls(&expr_match.expr, calls);
                for arm in &expr_match.arms {
                    expr_calls(&arm.body, calls);
                }
            }
            Expr::Loop(expr_loop) => {
                count_calls(&expr_loop.body.stmts, calls);
            }
            Expr::While(expr_while) => {
                expr_calls(&expr_while.cond, calls);
                count_calls(&expr_while.body.stmts, calls);
            }
            Expr::ForLoop(expr_for) => {
                expr_calls(&expr_for.expr, calls);
                count_calls(&expr_for.body.stmts, calls);
            }
            Expr::Block(expr_block) => {
                count_calls(&expr_block.block.stmts, calls);
            }
            _ => {}
        }
    }

    let mut calls = HashSet::new();
    count_calls(&block.stmts, &mut calls);
    let mut names: Vec<String> = calls.into_iter().collect();
    names.sort();
    names
}

/// Calculate non-structured exits for Rust function
fn rust_non_structured_exits(block: &syn::Block) -> usize {
    use syn::{Expr, ExprMethodCall, Stmt};

    fn count_exits(stmts: &[Stmt], count: &mut usize, is_tail: bool) {
        for (i, stmt) in stmts.iter().enumerate() {
            let is_last = i == stmts.len() - 1;
            match stmt {
                Stmt::Expr(expr, _) => {
                    expr_exits(expr, count, is_tail && is_last);
                }
                Stmt::Local(local) => {
                    if let Some(init) = &local.init {
                        expr_exits(&init.expr, count, false);
                    }
                }
                _ => {}
            }
        }
    }

    fn expr_exits(expr: &Expr, count: &mut usize, is_tail: bool) {
        match expr {
            Expr::Return(_) if !is_tail => {
                *count += 1;
            }
            Expr::Try(_) => {
                // ? operator counts as early return
                *count += 1;
            }
            Expr::MethodCall(ExprMethodCall { method, .. }) => {
                // unwrap, expect, panic count as non-structured exits
                let method_name = method.to_string();
                if matches!(
                    method_name.as_str(),
                    "unwrap" | "expect" | "unwrap_or_else" | "unwrap_or"
                ) {
                    *count += 1;
                }
            }
            Expr::Macro(expr_macro) => {
                // panic!, unreachable!, unimplemented! count as exits
                if let Some(segment) = expr_macro.mac.path.segments.last() {
                    let macro_name = segment.ident.to_string();
                    if matches!(
                        macro_name.as_str(),
                        "panic" | "unreachable" | "unimplemented" | "todo"
                    ) {
                        *count += 1;
                    }
                }
            }
            Expr::If(expr_if) => {
                expr_exits(&expr_if.cond, count, false);
                count_exits(&expr_if.then_branch.stmts, count, false);
                if let Some((_, else_expr)) = &expr_if.else_branch {
                    expr_exits(else_expr, count, false);
                }
            }
            Expr::Match(expr_match) => {
                expr_exits(&expr_match.expr, count, false);
                for arm in &expr_match.arms {
                    expr_exits(&arm.body, count, false);
                }
            }
            Expr::Loop(expr_loop) => {
                count_exits(&expr_loop.body.stmts, count, false);
            }
            Expr::While(expr_while) => {
                expr_exits(&expr_while.cond, count, false);
                count_exits(&expr_while.body.stmts, count, false);
            }
            Expr::ForLoop(expr_for) => {
                expr_exits(&expr_for.expr, count, false);
                count_exits(&expr_for.body.stmts, count, false);
            }
            Expr::Block(expr_block) => {
                count_exits(&expr_block.block.stmts, count, is_tail);
            }
            _ => {}
        }
    }

    let mut count = 0;
    count_exits(&block.stmts, &mut count, true);
    count
}

/// Count CC extras for Rust (match arms, boolean operators)
fn rust_count_cc_extras(block: &syn::Block) -> usize {
    use syn::{BinOp, Expr, Stmt};

    fn count_extras(stmts: &[Stmt], count: &mut usize) {
        for stmt in stmts {
            match stmt {
                Stmt::Expr(expr, _) => expr_extras(expr, count),
                Stmt::Local(local) => {
                    if let Some(init) = &local.init {
                        expr_extras(&init.expr, count);
                    }
                }
                _ => {}
            }
        }
    }

    fn expr_extras(expr: &Expr, count: &mut usize) {
        match expr {
            Expr::Match(expr_match) => {
                // Each match arm is a decision point
                *count += expr_match.arms.len();
                expr_extras(&expr_match.expr, count);
                for arm in &expr_match.arms {
                    expr_extras(&arm.body, count);
                }
            }
            Expr::Binary(expr_binary) => {
                // Boolean operators
                if matches!(expr_binary.op, BinOp::And(_) | BinOp::Or(_)) {
                    *count += 1;
                }
                expr_extras(&expr_binary.left, count);
                expr_extras(&expr_binary.right, count);
            }
            Expr::If(expr_if) => {
                expr_extras(&expr_if.cond, count);
                count_extras(&expr_if.then_branch.stmts, count);
                if let Some((_, else_expr)) = &expr_if.else_branch {
                    expr_extras(else_expr, count);
                }
            }
            Expr::Loop(expr_loop) => {
                count_extras(&expr_loop.body.stmts, count);
            }
            Expr::While(expr_while) => {
                expr_extras(&expr_while.cond, count);
                count_extras(&expr_while.body.stmts, count);
            }
            Expr::ForLoop(expr_for) => {
                expr_extras(&expr_for.expr, count);
                count_extras(&expr_for.body.stmts, count);
            }
            Expr::Block(expr_block) => {
                count_extras(&expr_block.block.stmts, count);
            }
            _ => {}
        }
    }

    let mut count = 0;
    count_extras(&block.stmts, &mut count);
    count
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::language::{
        CfgBuilder, ECMAScriptCfgBuilder, ECMAScriptParser, GoCfgBuilder, GoParser, JavaCfgBuilder,
        JavaParser, LanguageParser, PythonCfgBuilder, PythonParser, RustCfgBuilder,
    };

    /// Helper: parse Go source, discover functions, return (FunctionNode, Cfg) for the first.
    fn go_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
        let parser = GoParser::new().unwrap();
        let module = parser.parse(source, "test.go").unwrap();
        let functions = module.discover_functions(0, source);
        assert!(!functions.is_empty(), "expected at least one Go function");
        let func = functions.into_iter().next().unwrap();
        let cfg = GoCfgBuilder.build(&func);
        (func, cfg)
    }

    /// Helper: parse Java source, discover functions, return (FunctionNode, Cfg) for the first.
    fn java_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
        let parser = JavaParser::new().unwrap();
        let module = parser.parse(source, "Test.java").unwrap();
        let functions = module.discover_functions(0, source);
        assert!(!functions.is_empty(), "expected at least one Java function");
        let func = functions.into_iter().next().unwrap();
        let cfg = JavaCfgBuilder.build(&func);
        (func, cfg)
    }

    /// Helper: parse Python source, discover functions, return (FunctionNode, Cfg) for the first.
    fn python_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
        let parser = PythonParser::new().unwrap();
        let module = parser.parse(source, "test.py").unwrap();
        let functions = module.discover_functions(0, source);
        assert!(
            !functions.is_empty(),
            "expected at least one Python function"
        );
        let func = functions.into_iter().next().unwrap();
        let cfg = PythonCfgBuilder.build(&func);
        (func, cfg)
    }

    /// Helper: parse TypeScript source, discover functions, return (FunctionNode, Cfg) for the first.
    fn ecmascript_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
        use swc_common::{sync::Lrc, SourceMap};
        let source_map: Lrc<SourceMap> = Default::default();
        let parser = ECMAScriptParser::new(source_map);
        let module = parser.parse(source, "test.ts").unwrap();
        let functions = module.discover_functions(0, source);
        assert!(
            !functions.is_empty(),
            "expected at least one ECMAScript function"
        );
        let func = functions.into_iter().next().unwrap();
        let cfg = ECMAScriptCfgBuilder.build(&func);
        (func, cfg)
    }

    /// Helper: build a Rust FunctionNode + Cfg from a function source string.
    fn rust_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
        use crate::ast::FunctionId;
        use crate::language::{FunctionBody, SourceSpan};
        let func = crate::ast::FunctionNode {
            id: FunctionId {
                file_index: 0,
                local_index: 0,
            },
            name: Some("test".to_string()),
            span: SourceSpan::new(0, source.len(), 1, 1, 0),
            body: FunctionBody::Rust {
                source: source.to_string(),
            },
            suppression_reason: None,
        };
        let cfg = RustCfgBuilder.build(&func);
        (func, cfg)
    }

    // ── Go ─────────────────────────────────────────────────────────────────

    #[test]
    fn test_extract_go_simple_function() {
        let source = r#"package main
func hello() {
    println("hi")
}
"#;
        let (func, cfg) = go_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(m.cc >= 1, "CC must be at least 1, got {}", m.cc);
        assert_eq!(m.nd, 0, "no nesting");
        assert_eq!(m.ns, 0, "no non-structured exits");
        assert!(m.loc >= 3, "at least 3 lines");
        assert!(m.callee_names.contains(&"println".to_string()));
        assert_eq!(m.fo, m.callee_names.len());
    }

    #[test]
    fn test_extract_go_if_increments_cc_and_nd() {
        let source = r#"package main
func check(x int) string {
    if x > 0 {
        if x > 100 {
            return "big"
        }
        return "positive"
    }
    return "non-positive"
}
"#;
        let (func, cfg) = go_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(m.cc >= 2, "two branches → CC >= 2, got {}", m.cc);
        assert!(m.nd >= 2, "nested ifs → ND >= 2, got {}", m.nd);
        assert!(m.ns >= 2, "multiple returns → NS >= 2, got {}", m.ns);
    }

    #[test]
    fn test_extract_go_callee_names_and_fanout() {
        let source = r#"package main
import "fmt"
func doWork() {
    fmt.Println("a")
    fmt.Println("b")
    len("x")
}
"#;
        let (func, cfg) = go_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        // callee_names is a deduplicated set
        assert!(
            m.callee_names.contains(&"fmt.Println".to_string())
                || m.callee_names.iter().any(|n| n.contains("Println")),
            "expected Println in callees: {:?}",
            m.callee_names
        );
        assert_eq!(m.fo, m.callee_names.len(), "fo == callee_names.len()");
    }

    #[test]
    fn test_extract_go_defer_counts_as_ns() {
        let source = r#"package main
func withDefer() {
    defer cleanup()
    doWork()
}
"#;
        let (func, cfg) = go_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(
            m.ns >= 1,
            "defer should count as non-structured exit, got {}",
            m.ns
        );
    }

    #[test]
    fn test_extract_go_fallback_on_bad_source() {
        // A FunctionNode whose body source is empty/unparseable yields the fallback metrics.
        use crate::ast::FunctionId;
        use crate::language::{FunctionBody, SourceSpan};
        let func = crate::ast::FunctionNode {
            id: FunctionId {
                file_index: 0,
                local_index: 0,
            },
            name: Some("bad".to_string()),
            span: SourceSpan::new(0, 0, 1, 1, 0),
            body: FunctionBody::Go {
                body_node: 0,
                source: String::new(),
            },
            suppression_reason: None,
        };
        let cfg = crate::cfg::Cfg::new();
        let m = extract_metrics(&func, &cfg);
        assert_eq!(m.cc, 1, "fallback cc");
        assert_eq!(m.nd, 0);
        assert_eq!(m.fo, 0);
        assert_eq!(m.ns, 0);
        assert_eq!(m.loc, 0);
        assert!(m.callee_names.is_empty());
    }

    // ── Java ────────────────────────────────────────────────────────────────

    #[test]
    fn test_extract_java_simple_method() {
        let source = r#"class Foo {
    void greet() {
        System.out.println("hello");
    }
}
"#;
        let (func, cfg) = java_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(m.cc >= 1, "CC must be at least 1, got {}", m.cc);
        assert_eq!(m.nd, 0, "no nesting");
        assert!(m.loc >= 3);
        assert!(m.fo >= 1, "at least one callee");
    }

    #[test]
    fn test_extract_java_if_increments_cc_and_nd() {
        let source = r#"class Foo {
    String classify(int x) {
        if (x > 0) {
            if (x > 100) {
                return "big";
            }
            return "positive";
        }
        return "non-positive";
    }
}
"#;
        let (func, cfg) = java_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(m.cc >= 2, "two branches → CC >= 2, got {}", m.cc);
        assert!(m.nd >= 2, "nested ifs → ND >= 2, got {}", m.nd);
        assert!(m.ns >= 2, "multiple returns → NS >= 2, got {}", m.ns);
    }

    #[test]
    fn test_extract_java_ternary_increments_cc() {
        let source = r#"class Foo {
    int sign(int x) {
        return x > 0 ? 1 : -1;
    }
}
"#;
        let (func, cfg) = java_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        // ternary adds 1 to CC via java_count_cc_extras
        assert!(m.cc >= 2, "ternary → CC >= 2, got {}", m.cc);
    }

    #[test]
    fn test_extract_java_fallback_on_bad_source() {
        use crate::ast::FunctionId;
        use crate::language::{FunctionBody, SourceSpan};
        let func = crate::ast::FunctionNode {
            id: FunctionId {
                file_index: 0,
                local_index: 0,
            },
            name: Some("bad".to_string()),
            span: SourceSpan::new(0, 0, 1, 1, 0),
            body: FunctionBody::Java {
                body_node: 0,
                source: String::new(),
            },
            suppression_reason: None,
        };
        let cfg = crate::cfg::Cfg::new();
        let m = extract_metrics(&func, &cfg);
        assert_eq!(m.cc, 1);
        assert_eq!(m.fo, 0);
        assert!(m.callee_names.is_empty());
    }

    // ── Python ──────────────────────────────────────────────────────────────

    #[test]
    fn test_extract_python_simple_function() {
        let source = r#"def greet():
    print("hello")
"#;
        let (func, cfg) = python_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(m.cc >= 1, "CC must be at least 1, got {}", m.cc);
        assert_eq!(m.nd, 0, "no nesting");
        assert!(m.loc >= 2);
        assert!(m.callee_names.contains(&"print".to_string()));
        assert_eq!(m.fo, m.callee_names.len());
    }

    #[test]
    fn test_extract_python_if_increments_cc_and_nd() {
        let source = r#"def classify(x):
    if x > 0:
        if x > 100:
            return "big"
        return "positive"
    return "non-positive"
"#;
        let (func, cfg) = python_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(m.cc >= 2, "two branches → CC >= 2, got {}", m.cc);
        assert!(m.nd >= 2, "nested ifs → ND >= 2, got {}", m.nd);
        assert!(m.ns >= 2, "multiple returns → NS >= 2, got {}", m.ns);
    }

    #[test]
    fn test_extract_python_callee_names_and_fanout() {
        let source = r#"def do_work():
    foo()
    bar()
    foo()
"#;
        let (func, cfg) = python_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        // foo appears twice but callee_names is deduplicated
        assert!(
            m.callee_names.contains(&"foo".to_string()),
            "callee_names: {:?}",
            m.callee_names
        );
        assert!(
            m.callee_names.contains(&"bar".to_string()),
            "callee_names: {:?}",
            m.callee_names
        );
        assert_eq!(m.fo, m.callee_names.len(), "fo == callee_names.len()");
    }

    #[test]
    fn test_extract_python_fallback_on_bad_source() {
        use crate::ast::FunctionId;
        use crate::language::{FunctionBody, SourceSpan};
        let func = crate::ast::FunctionNode {
            id: FunctionId {
                file_index: 0,
                local_index: 0,
            },
            name: Some("bad".to_string()),
            span: SourceSpan::new(0, 0, 1, 1, 0),
            body: FunctionBody::Python {
                body_node: 0,
                source: String::new(),
            },
            suppression_reason: None,
        };
        let cfg = crate::cfg::Cfg::new();
        let m = extract_metrics(&func, &cfg);
        assert_eq!(m.cc, 1);
        assert_eq!(m.fo, 0);
        assert!(m.callee_names.is_empty());
    }

    // ── ECMAScript ──────────────────────────────────────────────────────────

    #[test]
    fn test_extract_ecmascript_callee_names_direct_call() {
        let source = r#"function doWork() { foo(); bar(); foo(); }"#;
        let (func, cfg) = ecmascript_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        // foo appears twice but callee_names is deduplicated
        assert!(
            m.callee_names.contains(&"foo".to_string()),
            "callee_names: {:?}",
            m.callee_names
        );
        assert!(
            m.callee_names.contains(&"bar".to_string()),
            "callee_names: {:?}",
            m.callee_names
        );
        assert_eq!(m.fo, m.callee_names.len(), "fo == callee_names.len()");
    }

    #[test]
    fn test_extract_ecmascript_callee_names_method_call() {
        let source = r#"function doWork() { obj.method(); console.log("x"); }"#;
        let (func, cfg) = ecmascript_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(m.fo >= 1, "method calls counted in fo");
        assert_eq!(m.fo, m.callee_names.len());
    }

    #[test]
    fn test_extract_ecmascript_no_calls() {
        let source = r#"function pure(x: number) { return x + 1; }"#;
        let (func, cfg) = ecmascript_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert_eq!(m.fo, 0, "no calls → fo=0");
        assert!(m.callee_names.is_empty(), "no calls → empty callee_names");
    }

    #[test]
    fn test_extract_ecmascript_computed_callee_filtered() {
        // Dynamic calls like arr[0]() produce <computed> — should be filtered
        let source = r#"function dyn(arr: any[]) { arr[0](); }"#;
        let (func, cfg) = ecmascript_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        // <computed> should not appear in callee_names
        assert!(
            !m.callee_names.iter().any(|n| n == "<computed>"),
            "computed callees must be filtered: {:?}",
            m.callee_names
        );
    }

    // ── Rust ────────────────────────────────────────────────────────────────

    #[test]
    fn test_extract_rust_callee_names_function_call() {
        let source = r#"fn do_work() { foo(); bar(); foo(); }"#;
        let (func, cfg) = rust_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(
            m.callee_names.contains(&"foo".to_string()),
            "callee_names: {:?}",
            m.callee_names
        );
        assert!(
            m.callee_names.contains(&"bar".to_string()),
            "callee_names: {:?}",
            m.callee_names
        );
        assert_eq!(m.fo, m.callee_names.len(), "fo == callee_names.len()");
    }

    #[test]
    fn test_extract_rust_callee_names_method_call() {
        let source = r#"fn do_work(v: Vec<i32>) -> usize { v.len() }"#;
        let (func, cfg) = rust_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(
            m.callee_names.contains(&"len".to_string()),
            "method call in callee_names: {:?}",
            m.callee_names
        );
        assert_eq!(m.fo, m.callee_names.len());
    }

    #[test]
    fn test_extract_rust_callee_names_macro_call() {
        let source = r#"fn greet() { println!("hello"); }"#;
        let (func, cfg) = rust_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert!(
            m.callee_names.contains(&"println".to_string()),
            "macro in callee_names: {:?}",
            m.callee_names
        );
        assert_eq!(m.fo, m.callee_names.len());
    }

    #[test]
    fn test_extract_rust_no_calls() {
        let source = r#"fn pure(x: i32) -> i32 { x + 1 }"#;
        let (func, cfg) = rust_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        assert_eq!(m.fo, 0, "no calls → fo=0");
        assert!(m.callee_names.is_empty(), "no calls → empty callee_names");
    }

    #[test]
    fn test_extract_rust_deduplication() {
        let source = r#"fn work() { foo(); foo(); foo(); bar(); }"#;
        let (func, cfg) = rust_function_and_cfg(source);
        let m = extract_metrics(&func, &cfg);
        // foo called 3 times, bar once — deduplicated to 2 unique callees
        assert_eq!(m.fo, 2, "deduplicated: foo+bar = 2");
        assert_eq!(m.callee_names, vec!["bar", "foo"], "sorted callee_names");
    }
}
