//! Core analysis functions for function scope capture

use crate::ast::ASTNode;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::ValueId;
use std::collections::BTreeSet;

use super::helpers::*;
use super::types::{CapturedEnv, CapturedKind, CapturedVar};

/// Analyzes function-scoped variables that can be safely captured for loop conditions/body.
///
/// # Phase 200-B Implementation
///
/// Detects function-scoped variables that are effectively immutable constants
/// within a loop context (e.g., `digits` in JsonParser._atoi()).
///
/// # Detection Criteria
///
/// A variable is captured if ALL of the following conditions are met:
///
/// 1. **Declared before the loop**: Variable must be declared in function scope before the loop
/// 2. **Safe constant init**: Initialized with string/integer literal only
/// 3. **Never reassigned**: Variable is never reassigned within the function (is_immutable = true)
/// 4. **Referenced in loop**: Variable is referenced in loop condition or body
/// 5. **Not a loop parameter**: Variable is not in scope.loop_params
/// 6. **Not a body-local**: Variable is not in scope.body_locals
///
/// # Example
///
/// ```nyash
/// method _atoi(s, pos, len) {
///     local digits = "0123456789"  // ✅ Captured (declared before loop, never reassigned)
///     local value = 0               // ❌ Not captured (reassigned in loop body)
///     loop(pos < len) {
///         local ch = s.charAt(pos)  // ❌ Not captured (body-local)
///         local digit = digits.indexOf(ch)
///         value = value * 10 + digit
///         pos = pos + 1
///     }
/// }
/// ```
///
/// # Arguments
///
/// * `fn_body` - AST nodes of the function body (for analysis)
/// * `loop_ast` - AST node of the loop statement
/// * `scope` - LoopScopeShape (for excluding loop params and body-locals)
///
/// # Returns
///
/// `CapturedEnv` containing all captured variables
#[allow(dead_code)]
pub(crate) fn analyze_captured_vars(
    fn_body: &[ASTNode],
    loop_ast: &ASTNode,
    scope: &LoopScopeShape,
) -> CapturedEnv {
    use std::env;

    let debug = env::var("NYASH_CAPTURE_DEBUG").is_ok();

    if debug {
        eprintln!("[capture/debug] Starting capture analysis");
    }

    // Step 1: Find loop position in fn_body
    let loop_index = match find_stmt_index(fn_body, loop_ast) {
        Some(idx) => idx,
        None => {
            if debug {
                eprintln!(
                    "[capture/debug] Loop not found in function body, returning empty CapturedEnv"
                );
            }
            return CapturedEnv::new();
        }
    };

    if debug {
        eprintln!("[capture/debug] Loop found at index {}", loop_index);
    }

    // Step 2: Collect local declarations BEFORE the loop
    let pre_loop_locals = collect_local_declarations(&fn_body[..loop_index]);

    if debug {
        eprintln!(
            "[capture/debug] Found {} pre-loop local declarations",
            pre_loop_locals.len()
        );
    }

    let mut env = CapturedEnv::new();

    // Step 3: For each pre-loop local, check capture criteria
    for (name, init_expr) in pre_loop_locals {
        if debug {
            eprintln!("[capture/check] Checking variable '{}'", name);
        }

        // 3a: Is init expression a safe constant?
        if !is_safe_const_init(&init_expr) {
            if debug {
                eprintln!("[capture/reject] '{}': init is not a safe constant", name);
            }
            continue;
        }

        // 3b: Is this variable reassigned anywhere in fn_body?
        if is_reassigned_in_fn(fn_body, &name) {
            if debug {
                eprintln!("[capture/reject] '{}': reassigned in function", name);
            }
            continue;
        }

        // 3c: Is this variable used in loop (condition or body)?
        if !is_used_in_loop(loop_ast, &name) {
            if debug {
                eprintln!("[capture/reject] '{}': not used in loop", name);
            }
            continue;
        }

        // 3d: Skip if already in pinned, carriers, or body_locals
        if scope.pinned.contains(&name) {
            if debug {
                eprintln!("[capture/reject] '{}': is a pinned variable", name);
            }
            continue;
        }

        if scope.carriers.contains(&name) {
            if debug {
                eprintln!("[capture/reject] '{}': is a carrier variable", name);
            }
            continue;
        }

        if scope.body_locals.contains(&name) {
            if debug {
                eprintln!("[capture/reject] '{}': is a body-local variable", name);
            }
            continue;
        }

        // All checks passed: add to CapturedEnv
        // Note: We don't have access to variable_map here, so we use a placeholder ValueId
        // The actual host_id will be resolved in ConditionEnvBuilder
        if debug {
            eprintln!(
                "[capture/accept] '{}': ALL CHECKS PASSED, adding to CapturedEnv",
                name
            );
        }

        env.add_var(CapturedVar {
            name: name.clone(),
            host_id: ValueId(0), // Placeholder, will be resolved in ConditionEnvBuilder
            is_immutable: true,
            kind: CapturedKind::Explicit,
        });
    }

    if debug {
        eprintln!(
            "[capture/result] Captured {} variables: {:?}",
            env.vars.len(),
            env.vars.iter().map(|v| &v.name).collect::<Vec<_>>()
        );
    }

    env
}

/// Phase 200-C: Analyze captured vars with condition/body instead of loop_ast
///
/// This variant solves the pointer comparison problem when the loop AST is constructed
/// dynamically (e.g., in Pattern 2). Instead of passing a loop_ast reference,
/// we pass the condition and body directly and perform structural matching.
///
/// # Arguments
///
/// * `fn_body` - AST nodes of the function body (for analysis)
/// * `loop_condition` - Condition expression of the loop
/// * `loop_body` - Body statements of the loop
/// * `scope` - LoopScopeShape (for excluding loop params and body-locals)
///
/// # Returns
///
/// `CapturedEnv` containing all captured variables
#[allow(dead_code)]
pub(crate) fn analyze_captured_vars_v2(
    fn_body: &[ASTNode],
    loop_condition: &ASTNode,
    loop_body: &[ASTNode],
    scope: &LoopScopeShape,
) -> CapturedEnv {
    use std::env;

    let debug = env::var("NYASH_CAPTURE_DEBUG").is_ok();

    if debug {
        eprintln!("[capture/debug] Starting capture analysis v2 (structural matching)");
    }

    // Step 1: Find loop position in fn_body by structural matching
    let loop_index = find_loop_index_by_structure(fn_body, loop_condition, loop_body);

    if debug {
        match loop_index {
            Some(idx) => eprintln!("[capture/debug] Loop found at index {} by structure", idx),
            None => eprintln!("[capture/debug] Loop not found in function body by structure (may be unit test or synthetic case)"),
        }
    }

    // Step 2: Collect local declarations BEFORE the loop
    let pre_loop_locals = if let Some(idx) = loop_index {
        collect_local_declarations(&fn_body[..idx])
    } else {
        // No loop found in fn_body - might be a unit test or synthetic case
        // Still collect all locals from fn_body
        collect_local_declarations(fn_body)
    };

    if debug {
        eprintln!(
            "[capture/debug] Found {} pre-loop local declarations",
            pre_loop_locals.len()
        );
    }

    let mut env = CapturedEnv::new();

    // Step 3: For each pre-loop local, check capture criteria
    for (name, init_expr) in &pre_loop_locals {
        if debug {
            eprintln!("[capture/check] Checking variable '{}'", name);
        }

        // 3a: Is init expression a safe constant?
        if !is_safe_const_init(init_expr) {
            if debug {
                eprintln!("[capture/reject] '{}': init is not a safe constant", name);
            }
            continue;
        }

        // 3b: Is this variable reassigned anywhere in fn_body?
        if is_reassigned_in_fn(fn_body, name) {
            if debug {
                eprintln!("[capture/reject] '{}': reassigned in function", name);
            }
            continue;
        }

        // 3c: Is this variable used in loop (condition or body)?
        if !is_used_in_loop_parts(loop_condition, loop_body, name) {
            if debug {
                eprintln!("[capture/reject] '{}': not used in loop", name);
            }
            continue;
        }

        // 3d: Skip if already in pinned, carriers, or body_locals
        if scope.pinned.contains(name) {
            if debug {
                eprintln!("[capture/reject] '{}': is a pinned variable", name);
            }
            continue;
        }

        if scope.carriers.contains(name) {
            if debug {
                eprintln!("[capture/reject] '{}': is a carrier variable", name);
            }
            continue;
        }

        if scope.body_locals.contains(name) {
            if debug {
                eprintln!("[capture/reject] '{}': is a body-local variable", name);
            }
            continue;
        }

        // All checks passed: add to CapturedEnv
        if debug {
            eprintln!(
                "[capture/accept] '{}': ALL CHECKS PASSED, adding to CapturedEnv",
                name
            );
        }

        env.add_var(CapturedVar {
            name: name.clone(),
            host_id: ValueId(0), // Placeholder, will be resolved in ConditionEnvBuilder
            is_immutable: true,
            kind: CapturedKind::Explicit,
        });
    }

    // Phase 245C: Capture function parameters used in loop
    let names_in_loop = collect_names_in_loop_parts(loop_condition, loop_body);

    // pre-loop local names (already processed above)
    let pre_loop_local_names: BTreeSet<String> = pre_loop_locals
        .iter()
        .map(|(name, _)| name.clone())
        .collect();

    // Check each variable used in loop
    for name in names_in_loop {
        // Skip if already processed as pre-loop local
        if pre_loop_local_names.contains(&name) {
            continue;
        }

        // Skip if already in pinned, carriers, or body_locals
        if scope.pinned.contains(&name)
            || scope.carriers.contains(&name)
            || scope.body_locals.contains(&name)
        {
            continue;
        }

        // Skip if reassigned in function (function parameters should not be reassigned)
        if is_reassigned_in_fn(fn_body, &name) {
            if debug {
                eprintln!("[capture/param/reject] '{}': reassigned in function", name);
            }
            continue;
        }

        // This is a function parameter-like variable - add to CapturedEnv
        if debug {
            eprintln!(
                "[capture/param/accept] '{}': function parameter used in loop",
                name
            );
        }

        env.add_var(CapturedVar {
            name: name.clone(),
            host_id: ValueId(0), // Placeholder, will be resolved in ConditionEnvBuilder
            is_immutable: true,
            kind: CapturedKind::Explicit,
        });
    }

    if debug {
        eprintln!(
            "[capture/result] Captured {} variables: {:?}",
            env.vars.len(),
            env.vars.iter().map(|v| &v.name).collect::<Vec<_>>()
        );
    }

    env
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
    use crate::mir::BasicBlockId;
    use std::collections::{BTreeMap, BTreeSet};

    // Phase 200-B: Capture analysis tests

    #[test]
    fn test_capture_simple_digits() {
        // Build AST for:
        // local digits = "0123456789"
        // loop(i < 10) {
        //     local pos = digits.indexOf(ch)
        // }

        let digits_decl = ASTNode::Local {
            variables: vec!["digits".to_string()],
            initial_values: vec![Some(Box::new(ASTNode::Literal {
                value: LiteralValue::String("0123456789".to_string()),
                span: Span::unknown(),
            }))],
            span: Span::unknown(),
        };

        let loop_body = vec![ASTNode::Local {
            variables: vec!["pos".to_string()],
            initial_values: vec![Some(Box::new(ASTNode::MethodCall {
                object: Box::new(ASTNode::Variable {
                    name: "digits".to_string(),
                    span: Span::unknown(),
                }),
                method: "indexOf".to_string(),
                arguments: vec![ASTNode::Variable {
                    name: "ch".to_string(),
                    span: Span::unknown(),
                }],
                span: Span::unknown(),
            }))],
            span: Span::unknown(),
        }];

        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: crate::ast::BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "i".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(10),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: loop_body,
            span: Span::unknown(),
        };

        let fn_body = vec![digits_decl, loop_node.clone()];

        let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
            header: BasicBlockId(0),
            body: BasicBlockId(1),
            latch: BasicBlockId(2),
            exit: BasicBlockId(3),
            pinned: BTreeSet::from(["i".to_string()]),
            carriers: BTreeSet::new(),
            body_locals: BTreeSet::from(["pos".to_string()]),
            exit_live: BTreeSet::new(),
            progress_carrier: None,
            variable_definitions: BTreeMap::new(),
        };

        // IMPORTANT: Pass a reference to the same loop_node instance that's in fn_body
        // find_stmt_index uses pointer comparison, so we must use &fn_body[1] instead of &loop_node
        let env = analyze_captured_vars(&fn_body, &fn_body[1], &scope);

        assert_eq!(env.vars.len(), 1);
        assert!(env.get("digits").is_some());
        let var = env.get("digits").unwrap();
        assert_eq!(var.name, "digits");
        assert!(var.is_immutable);
    }

    #[test]
    fn test_capture_reassigned_rejected() {
        // Build AST for:
        // local digits = "0123456789"
        // digits = "abc"  // reassignment
        // loop(i < 10) {
        //     local pos = digits.indexOf(ch)
        // }

        let digits_decl = ASTNode::Local {
            variables: vec!["digits".to_string()],
            initial_values: vec![Some(Box::new(ASTNode::Literal {
                value: LiteralValue::String("0123456789".to_string()),
                span: Span::unknown(),
            }))],
            span: Span::unknown(),
        };

        let reassignment = ASTNode::Assignment {
            target: Box::new(ASTNode::Variable {
                name: "digits".to_string(),
                span: Span::unknown(),
            }),
            value: Box::new(ASTNode::Literal {
                value: LiteralValue::String("abc".to_string()),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };

        let loop_body = vec![ASTNode::Local {
            variables: vec!["pos".to_string()],
            initial_values: vec![Some(Box::new(ASTNode::MethodCall {
                object: Box::new(ASTNode::Variable {
                    name: "digits".to_string(),
                    span: Span::unknown(),
                }),
                method: "indexOf".to_string(),
                arguments: vec![],
                span: Span::unknown(),
            }))],
            span: Span::unknown(),
        }];

        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: crate::ast::BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "i".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(10),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: loop_body,
            span: Span::unknown(),
        };

        let fn_body = vec![digits_decl, reassignment, loop_node.clone()];

        let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
            header: BasicBlockId(0),
            body: BasicBlockId(1),
            latch: BasicBlockId(2),
            exit: BasicBlockId(3),
            pinned: BTreeSet::from(["i".to_string()]),
            carriers: BTreeSet::new(),
            body_locals: BTreeSet::from(["pos".to_string()]),
            exit_live: BTreeSet::new(),
            progress_carrier: None,
            variable_definitions: BTreeMap::new(),
        };

        let env = analyze_captured_vars(&fn_body, &loop_node, &scope);

        // Should reject because digits is reassigned
        assert_eq!(env.vars.len(), 0);
    }

    #[test]
    fn test_capture_after_loop_rejected() {
        // Build AST for:
        // loop(i < 10) { }
        // local digits = "0123456789"  // defined AFTER loop

        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: crate::ast::BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "i".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(10),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![],
            span: Span::unknown(),
        };

        let digits_decl = ASTNode::Local {
            variables: vec!["digits".to_string()],
            initial_values: vec![Some(Box::new(ASTNode::Literal {
                value: LiteralValue::String("0123456789".to_string()),
                span: Span::unknown(),
            }))],
            span: Span::unknown(),
        };

        let fn_body = vec![loop_node.clone(), digits_decl];

        let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
            header: BasicBlockId(0),
            body: BasicBlockId(1),
            latch: BasicBlockId(2),
            exit: BasicBlockId(3),
            pinned: BTreeSet::from(["i".to_string()]),
            carriers: BTreeSet::new(),
            body_locals: BTreeSet::new(),
            exit_live: BTreeSet::new(),
            progress_carrier: None,
            variable_definitions: BTreeMap::new(),
        };

        let env = analyze_captured_vars(&fn_body, &loop_node, &scope);

        // Should reject because digits is defined after the loop
        assert_eq!(env.vars.len(), 0);
    }

    #[test]
    fn test_capture_method_call_init_rejected() {
        // Build AST for:
        // local result = someBox.getValue()  // MethodCall init
        // loop(i < 10) {
        //     local x = result.something()
        // }

        let result_decl = ASTNode::Local {
            variables: vec!["result".to_string()],
            initial_values: vec![Some(Box::new(ASTNode::MethodCall {
                object: Box::new(ASTNode::Variable {
                    name: "someBox".to_string(),
                    span: Span::unknown(),
                }),
                method: "getValue".to_string(),
                arguments: vec![],
                span: Span::unknown(),
            }))],
            span: Span::unknown(),
        };

        let loop_body = vec![ASTNode::Local {
            variables: vec!["x".to_string()],
            initial_values: vec![Some(Box::new(ASTNode::MethodCall {
                object: Box::new(ASTNode::Variable {
                    name: "result".to_string(),
                    span: Span::unknown(),
                }),
                method: "something".to_string(),
                arguments: vec![],
                span: Span::unknown(),
            }))],
            span: Span::unknown(),
        }];

        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: crate::ast::BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "i".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(10),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: loop_body,
            span: Span::unknown(),
        };

        let fn_body = vec![result_decl, loop_node.clone()];

        let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
            header: BasicBlockId(0),
            body: BasicBlockId(1),
            latch: BasicBlockId(2),
            exit: BasicBlockId(3),
            pinned: BTreeSet::from(["i".to_string()]),
            carriers: BTreeSet::new(),
            body_locals: BTreeSet::from(["x".to_string()]),
            exit_live: BTreeSet::new(),
            progress_carrier: None,
            variable_definitions: BTreeMap::new(),
        };

        let env = analyze_captured_vars(&fn_body, &loop_node, &scope);

        // Should reject because result is initialized with MethodCall (not safe constant)
        assert_eq!(env.vars.len(), 0);
    }

    #[test]
    fn test_capture_unused_in_loop_rejected() {
        // Build AST for:
        // local digits = "0123456789"  // not used in loop
        // loop(i < 10) {
        //     print(i)  // doesn't use digits
        // }

        let digits_decl = ASTNode::Local {
            variables: vec!["digits".to_string()],
            initial_values: vec![Some(Box::new(ASTNode::Literal {
                value: LiteralValue::String("0123456789".to_string()),
                span: Span::unknown(),
            }))],
            span: Span::unknown(),
        };

        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: crate::ast::BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "i".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(10),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![], // empty body, no usage of digits
            span: Span::unknown(),
        };

        let fn_body = vec![digits_decl, loop_node.clone()];

        let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
            header: BasicBlockId(0),
            body: BasicBlockId(1),
            latch: BasicBlockId(2),
            exit: BasicBlockId(3),
            pinned: BTreeSet::from(["i".to_string()]),
            carriers: BTreeSet::new(),
            body_locals: BTreeSet::new(),
            exit_live: BTreeSet::new(),
            progress_carrier: None,
            variable_definitions: BTreeMap::new(),
        };

        let env = analyze_captured_vars(&fn_body, &loop_node, &scope);

        // Should reject because digits is not used in loop
        assert_eq!(env.vars.len(), 0);
    }

    // Phase 245C: Function parameter capture tests

    #[test]
    fn test_capture_function_param_used_in_condition() {
        // Simulate: fn parse_number(s, p, len) { loop(p < len) { ... } }
        // Expected: 'len' should be captured (used in condition, not reassigned)

        let condition = Box::new(ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(ASTNode::Variable {
                name: "p".to_string(),
                span: Span::unknown(),
            }),
            right: Box::new(ASTNode::Variable {
                name: "len".to_string(), // function parameter
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        });

        let body = vec![ASTNode::Assignment {
            target: Box::new(ASTNode::Variable {
                name: "p".to_string(),
                span: Span::unknown(),
            }),
            value: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Add,
                left: Box::new(ASTNode::Variable {
                    name: "p".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(1),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        }];

        let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
            header: BasicBlockId(0),
            body: BasicBlockId(1),
            latch: BasicBlockId(2),
            exit: BasicBlockId(3),
            pinned: BTreeSet::from(["p".to_string()]), // p is loop param
            carriers: BTreeSet::new(),
            body_locals: BTreeSet::new(),
            exit_live: BTreeSet::new(),
            progress_carrier: None,
            variable_definitions: BTreeMap::new(),
        };

        // Use analyze_captured_vars_v2 with structural matching
        let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);

        // Should capture 'len' (function parameter used in condition)
        assert_eq!(env.vars.len(), 1);
        assert!(env.get("len").is_some());
        let var = env.get("len").unwrap();
        assert_eq!(var.name, "len");
        assert!(var.is_immutable);
    }

    #[test]
    fn test_capture_function_param_used_in_method_call() {
        // Simulate: fn parse_number(s, p) { loop(p < s.length()) { ch = s.charAt(p) } }
        // Expected: 's' should be captured (used in condition and body, not reassigned)

        let condition = Box::new(ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(ASTNode::Variable {
                name: "p".to_string(),
                span: Span::unknown(),
            }),
            right: Box::new(ASTNode::MethodCall {
                object: Box::new(ASTNode::Variable {
                    name: "s".to_string(), // function parameter
                    span: Span::unknown(),
                }),
                method: "length".to_string(),
                arguments: vec![],
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        });

        let body = vec![
            ASTNode::Local {
                variables: vec!["ch".to_string()],
                initial_values: vec![Some(Box::new(ASTNode::MethodCall {
                    object: Box::new(ASTNode::Variable {
                        name: "s".to_string(), // function parameter
                        span: Span::unknown(),
                    }),
                    method: "charAt".to_string(),
                    arguments: vec![ASTNode::Variable {
                        name: "p".to_string(),
                        span: Span::unknown(),
                    }],
                    span: Span::unknown(),
                }))],
                span: Span::unknown(),
            },
            ASTNode::Assignment {
                target: Box::new(ASTNode::Variable {
                    name: "p".to_string(),
                    span: Span::unknown(),
                }),
                value: Box::new(ASTNode::BinaryOp {
                    operator: BinaryOperator::Add,
                    left: Box::new(ASTNode::Variable {
                        name: "p".to_string(),
                        span: Span::unknown(),
                    }),
                    right: Box::new(ASTNode::Literal {
                        value: LiteralValue::Integer(1),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            },
        ];

        let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
            header: BasicBlockId(0),
            body: BasicBlockId(1),
            latch: BasicBlockId(2),
            exit: BasicBlockId(3),
            pinned: BTreeSet::from(["p".to_string()]), // p is loop param
            carriers: BTreeSet::new(),
            body_locals: BTreeSet::from(["ch".to_string()]),
            exit_live: BTreeSet::new(),
            progress_carrier: None,
            variable_definitions: BTreeMap::new(),
        };

        // Use analyze_captured_vars_v2 with structural matching
        let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);

        // Should capture 's' (function parameter used in condition and body)
        assert_eq!(env.vars.len(), 1);
        assert!(env.get("s").is_some());
        let var = env.get("s").unwrap();
        assert_eq!(var.name, "s");
        assert!(var.is_immutable);
    }

    #[test]
    fn test_capture_function_param_reassigned_rejected() {
        // Simulate: fn bad_func(x) { x = 5; loop(x < 10) { x = x + 1 } }
        // Expected: 'x' should NOT be captured (reassigned in function)

        let condition = Box::new(ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(ASTNode::Variable {
                name: "x".to_string(),
                span: Span::unknown(),
            }),
            right: Box::new(ASTNode::Literal {
                value: LiteralValue::Integer(10),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        });

        let body = vec![ASTNode::Assignment {
            target: Box::new(ASTNode::Variable {
                name: "x".to_string(),
                span: Span::unknown(),
            }),
            value: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Add,
                left: Box::new(ASTNode::Variable {
                    name: "x".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(1),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        }];

        // fn_body includes reassignment before loop
        let fn_body = vec![ASTNode::Assignment {
            target: Box::new(ASTNode::Variable {
                name: "x".to_string(),
                span: Span::unknown(),
            }),
            value: Box::new(ASTNode::Literal {
                value: LiteralValue::Integer(5),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        }];

        let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
            header: BasicBlockId(0),
            body: BasicBlockId(1),
            latch: BasicBlockId(2),
            exit: BasicBlockId(3),
            pinned: BTreeSet::from(["x".to_string()]), // x is loop param
            carriers: BTreeSet::new(),
            body_locals: BTreeSet::new(),
            exit_live: BTreeSet::new(),
            progress_carrier: None,
            variable_definitions: BTreeMap::new(),
        };

        // Use analyze_captured_vars_v2 with structural matching
        let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);

        // Should NOT capture 'x' (reassigned in fn_body)
        assert_eq!(env.vars.len(), 0);
    }

    #[test]
    fn test_capture_mixed_locals_and_params() {
        // Simulate: fn parse(s, len) { local digits = "0123"; loop(p < len) { ch = digits.indexOf(...); s.charAt(...) } }
        // Expected: 'len', 's', and 'digits' should all be captured

        let condition = Box::new(ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(ASTNode::Variable {
                name: "p".to_string(),
                span: Span::unknown(),
            }),
            right: Box::new(ASTNode::Variable {
                name: "len".to_string(), // function parameter
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        });

        let body = vec![
            ASTNode::Local {
                variables: vec!["ch".to_string()],
                initial_values: vec![Some(Box::new(ASTNode::MethodCall {
                    object: Box::new(ASTNode::Variable {
                        name: "s".to_string(), // function parameter
                        span: Span::unknown(),
                    }),
                    method: "charAt".to_string(),
                    arguments: vec![ASTNode::Variable {
                        name: "p".to_string(),
                        span: Span::unknown(),
                    }],
                    span: Span::unknown(),
                }))],
                span: Span::unknown(),
            },
            ASTNode::Local {
                variables: vec!["digit".to_string()],
                initial_values: vec![Some(Box::new(ASTNode::MethodCall {
                    object: Box::new(ASTNode::Variable {
                        name: "digits".to_string(), // pre-loop local
                        span: Span::unknown(),
                    }),
                    method: "indexOf".to_string(),
                    arguments: vec![ASTNode::Variable {
                        name: "ch".to_string(),
                        span: Span::unknown(),
                    }],
                    span: Span::unknown(),
                }))],
                span: Span::unknown(),
            },
        ];

        // fn_body includes local declaration before loop
        let fn_body = vec![ASTNode::Local {
            variables: vec!["digits".to_string()],
            initial_values: vec![Some(Box::new(ASTNode::Literal {
                value: LiteralValue::String("0123".to_string()),
                span: Span::unknown(),
            }))],
            span: Span::unknown(),
        }];

        let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
            header: BasicBlockId(0),
            body: BasicBlockId(1),
            latch: BasicBlockId(2),
            exit: BasicBlockId(3),
            pinned: BTreeSet::from(["p".to_string()]), // p is loop param
            carriers: BTreeSet::new(),
            body_locals: BTreeSet::from(["ch".to_string(), "digit".to_string()]),
            exit_live: BTreeSet::new(),
            progress_carrier: None,
            variable_definitions: BTreeMap::new(),
        };

        // Use analyze_captured_vars_v2 with structural matching
        let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);

        // Should capture all three: 'len' (param), 's' (param), 'digits' (pre-loop local)
        assert_eq!(env.vars.len(), 3);
        assert!(env.get("len").is_some());
        assert!(env.get("s").is_some());
        assert!(env.get("digits").is_some());
    }
}
