//! Ownership Analyzer for real AST (`crate::ast::ASTNode`)
//!
//! Phase 63: analysis-only (dev-only via `normalized_dev` feature).

use super::*;
use crate::ast::ASTNode;
use std::collections::{BTreeMap, BTreeSet};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct BindingId(u32);

#[derive(Debug, Clone)]
struct BindingInfo {
    name: String,
    owner_scope: ScopeId,
}

#[derive(Debug)]
struct ScopeInfo {
    id: ScopeId,
    kind: ScopeKind,
    parent: Option<ScopeId>,
    declared: BTreeMap<String, BindingId>,
    reads: BTreeSet<BindingId>,
    writes: BTreeSet<BindingId>,
    condition_reads: BTreeSet<BindingId>,
}

/// Analyzes real AST and produces `OwnershipPlan`.
///
/// This analyzer:
/// - Treats `ASTNode::Local` as "definition" (no JSON v0 rebind hack)
/// - Records writes via `ASTNode::Assignment` / `ASTNode::GroupedAssignmentExpr`
/// - Treats `Loop/While/ForRange` and `If` conditions as `condition_reads`
pub struct AstOwnershipAnalyzer {
    scopes: BTreeMap<ScopeId, ScopeInfo>,
    bindings: BTreeMap<BindingId, BindingInfo>,
    next_scope_id: u32,
    next_binding_id: u32,
    env_stack: Vec<BTreeMap<String, BindingId>>,
}

impl AstOwnershipAnalyzer {
    pub fn new() -> Self {
        Self {
            scopes: BTreeMap::new(),
            bindings: BTreeMap::new(),
            next_scope_id: 0,
            next_binding_id: 0,
            env_stack: Vec::new(),
        }
    }

    pub fn analyze_ast(&mut self, ast: &ASTNode) -> Result<Vec<OwnershipPlan>, String> {
        self.scopes.clear();
        self.bindings.clear();
        self.next_scope_id = 0;
        self.next_binding_id = 0;
        self.env_stack.clear();

        match ast {
            ASTNode::Program { statements, .. } => {
                for stmt in statements {
                    self.analyze_toplevel(stmt, None)?;
                }
            }
            ASTNode::FunctionDeclaration { .. } => {
                self.analyze_function_decl(ast, None)?;
            }
            _ => {
                return Err(
                    "AstOwnershipAnalyzer: expected Program or FunctionDeclaration".to_string(),
                );
            }
        }

        self.build_plans()
    }

    fn analyze_toplevel(&mut self, node: &ASTNode, parent: Option<ScopeId>) -> Result<(), String> {
        match node {
            ASTNode::FunctionDeclaration { .. } => {
                self.analyze_function_decl(node, parent)?;
            }
            ASTNode::BoxDeclaration {
                methods,
                constructors,
                ..
            } => {
                for (_, f) in methods {
                    self.analyze_function_decl(f, parent)?;
                }
                for (_, f) in constructors {
                    self.analyze_function_decl(f, parent)?;
                }
            }
            _ => {}
        }
        Ok(())
    }

    fn alloc_scope(&mut self, kind: ScopeKind, parent: Option<ScopeId>) -> ScopeId {
        let id = ScopeId(self.next_scope_id);
        self.next_scope_id += 1;
        self.scopes.insert(
            id,
            ScopeInfo {
                id,
                kind,
                parent,
                declared: BTreeMap::new(),
                reads: BTreeSet::new(),
                writes: BTreeSet::new(),
                condition_reads: BTreeSet::new(),
            },
        );
        id
    }

    fn analyze_function_decl(
        &mut self,
        node: &ASTNode,
        parent: Option<ScopeId>,
    ) -> Result<ScopeId, String> {
        let ASTNode::FunctionDeclaration { params, body, .. } = node else {
            return Err("AstOwnershipAnalyzer: expected FunctionDeclaration".to_string());
        };

        let scope_id = self.alloc_scope(ScopeKind::Function, parent);

        self.push_env();
        for name in params {
            self.declare_binding(scope_id, name)?;
        }

        let result: Result<(), String> = body
            .iter()
            .try_for_each(|stmt| self.analyze_node(stmt, scope_id, false));
        self.pop_env();
        result?;

        Ok(scope_id)
    }

    fn analyze_node(
        &mut self,
        node: &ASTNode,
        current_scope: ScopeId,
        is_condition: bool,
    ) -> Result<(), String> {
        match node {
            ASTNode::Program { statements, .. } => {
                let block_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
                self.push_env();
                let result: Result<(), String> = statements
                    .iter()
                    .try_for_each(|s| self.analyze_node(s, block_scope, false));
                self.pop_env();
                result?;
                self.propagate_to_parent(block_scope);
            }

            ASTNode::FunctionDeclaration { .. } => {
                self.analyze_function_decl(node, Some(current_scope))?;
            }

            ASTNode::BoxDeclaration {
                methods,
                constructors,
                ..
            } => {
                for (_, f) in methods {
                    self.analyze_function_decl(f, Some(current_scope))?;
                }
                for (_, f) in constructors {
                    self.analyze_function_decl(f, Some(current_scope))?;
                }
            }

            ASTNode::Local {
                variables,
                initial_values,
                ..
            } => {
                for (name, init) in variables.iter().zip(initial_values.iter()) {
                    if let Some(expr) = init.as_ref() {
                        self.analyze_node(expr, current_scope, false)?;
                    }
                    self.declare_binding(current_scope, name)?;
                }
            }

            ASTNode::Outbox {
                variables,
                initial_values,
                ..
            } => {
                for (name, init) in variables.iter().zip(initial_values.iter()) {
                    if let Some(expr) = init.as_ref() {
                        self.analyze_node(expr, current_scope, false)?;
                    }
                    self.declare_binding(current_scope, name)?;
                }
            }

            ASTNode::Assignment { target, value, .. } => {
                self.record_assignment_target(target, current_scope)?;
                self.analyze_node(value, current_scope, false)?;
            }

            ASTNode::GroupedAssignmentExpr { lhs, rhs, .. } => {
                let binding = self.resolve_binding(lhs).ok_or_else(|| {
                    format!("AstOwnershipAnalyzer: write to undefined var '{}'", lhs)
                })?;
                self.record_write(binding, current_scope);
                self.analyze_node(rhs, current_scope, is_condition)?;
            }

            ASTNode::Nowait {
                variable,
                expression,
                ..
            } => {
                let binding = self.resolve_binding(variable).ok_or_else(|| {
                    format!(
                        "AstOwnershipAnalyzer: write to undefined var '{}'",
                        variable
                    )
                })?;
                self.record_write(binding, current_scope);
                self.analyze_node(expression, current_scope, false)?;
            }

            ASTNode::Print { expression, .. } => {
                self.analyze_node(expression, current_scope, false)?;
            }

            ASTNode::Return { value, .. } => {
                if let Some(v) = value.as_ref() {
                    self.analyze_node(v, current_scope, false)?;
                }
            }

            ASTNode::Break { .. } | ASTNode::Continue { .. } => {}

            ASTNode::If {
                condition,
                then_body,
                else_body,
                ..
            } => {
                let if_scope = self.alloc_scope(ScopeKind::If, Some(current_scope));
                self.analyze_node(condition, if_scope, true)?;

                let then_scope = self.alloc_scope(ScopeKind::Block, Some(if_scope));
                self.push_env();
                let result: Result<(), String> = then_body
                    .iter()
                    .try_for_each(|s| self.analyze_node(s, then_scope, false));
                self.pop_env();
                result?;
                self.propagate_to_parent(then_scope);

                if let Some(else_body) = else_body {
                    let else_scope = self.alloc_scope(ScopeKind::Block, Some(if_scope));
                    self.push_env();
                    let result: Result<(), String> = else_body
                        .iter()
                        .try_for_each(|s| self.analyze_node(s, else_scope, false));
                    self.pop_env();
                    result?;
                    self.propagate_to_parent(else_scope);
                }

                self.propagate_to_parent(if_scope);
            }

            ASTNode::Loop {
                condition, body, ..
            }
            | ASTNode::While {
                condition, body, ..
            } => {
                let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_scope));
                self.push_env();
                let result: Result<(), String> = (|| {
                    for s in body {
                        self.analyze_node(s, loop_scope, false)?;
                    }
                    self.analyze_node(condition, loop_scope, true)?;
                    Ok(())
                })();
                self.pop_env();
                result?;
                self.propagate_to_parent(loop_scope);
            }

            ASTNode::ForRange {
                var_name,
                start,
                end,
                body,
                ..
            } => {
                let loop_scope = self.alloc_scope(ScopeKind::Loop, Some(current_scope));
                self.push_env();
                let result: Result<(), String> = (|| {
                    self.declare_binding(loop_scope, var_name)?;
                    self.analyze_node(start, loop_scope, true)?;
                    self.analyze_node(end, loop_scope, true)?;
                    for s in body {
                        self.analyze_node(s, loop_scope, false)?;
                    }
                    Ok(())
                })();
                self.pop_env();
                result?;
                self.propagate_to_parent(loop_scope);
            }

            ASTNode::ScopeBox { body, .. } => {
                let block_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
                self.push_env();
                let result: Result<(), String> = body
                    .iter()
                    .try_for_each(|s| self.analyze_node(s, block_scope, false));
                self.pop_env();
                result?;
                self.propagate_to_parent(block_scope);
            }

            ASTNode::TryCatch {
                try_body,
                catch_clauses,
                finally_body,
                ..
            } => {
                let try_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
                self.push_env();
                let result: Result<(), String> = try_body
                    .iter()
                    .try_for_each(|s| self.analyze_node(s, try_scope, false));
                self.pop_env();
                result?;
                self.propagate_to_parent(try_scope);

                for clause in catch_clauses {
                    let catch_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
                    self.push_env();
                    if let Some(var) = clause.variable_name.as_ref() {
                        self.declare_binding(catch_scope, var)?;
                    }
                    let result: Result<(), String> = clause
                        .body
                        .iter()
                        .try_for_each(|s| self.analyze_node(s, catch_scope, false));
                    self.pop_env();
                    result?;
                    self.propagate_to_parent(catch_scope);
                }

                if let Some(finally_body) = finally_body {
                    let finally_scope = self.alloc_scope(ScopeKind::Block, Some(current_scope));
                    self.push_env();
                    let result: Result<(), String> = finally_body
                        .iter()
                        .try_for_each(|s| self.analyze_node(s, finally_scope, false));
                    self.pop_env();
                    result?;
                    self.propagate_to_parent(finally_scope);
                }
            }

            ASTNode::Throw { expression, .. } => {
                self.analyze_node(expression, current_scope, false)?;
            }

            ASTNode::UsingStatement { .. } | ASTNode::ImportStatement { .. } => {}

            ASTNode::GlobalVar { value, .. } => {
                self.analyze_node(value, current_scope, false)?;
            }

            ASTNode::Literal { .. }
            | ASTNode::This { .. }
            | ASTNode::Me { .. }
            | ASTNode::ThisField { .. }
            | ASTNode::MeField { .. } => {}

            ASTNode::Variable { name, .. } => {
                if let Some(binding) = self.resolve_binding(name) {
                    self.record_read(binding, current_scope, is_condition);
                }
            }

            ASTNode::UnaryOp { operand, .. } => {
                self.analyze_node(operand, current_scope, is_condition)?;
            }

            ASTNode::BinaryOp { left, right, .. } => {
                self.analyze_node(left, current_scope, is_condition)?;
                self.analyze_node(right, current_scope, is_condition)?;
            }

            ASTNode::MethodCall {
                object, arguments, ..
            } => {
                self.analyze_node(object, current_scope, is_condition)?;
                for a in arguments {
                    self.analyze_node(a, current_scope, is_condition)?;
                }
            }

            ASTNode::FieldAccess { object, .. } => {
                self.analyze_node(object, current_scope, is_condition)?;
            }

            ASTNode::Index { target, index, .. } => {
                self.analyze_node(target, current_scope, is_condition)?;
                self.analyze_node(index, current_scope, is_condition)?;
            }

            ASTNode::New { arguments, .. } => {
                for a in arguments {
                    self.analyze_node(a, current_scope, is_condition)?;
                }
            }

            ASTNode::FromCall { arguments, .. } => {
                for a in arguments {
                    self.analyze_node(a, current_scope, is_condition)?;
                }
            }

            ASTNode::FunctionCall { arguments, .. } => {
                for a in arguments {
                    self.analyze_node(a, current_scope, is_condition)?;
                }
            }

            ASTNode::Call {
                callee, arguments, ..
            } => {
                self.analyze_node(callee, current_scope, is_condition)?;
                for a in arguments {
                    self.analyze_node(a, current_scope, is_condition)?;
                }
            }

            ASTNode::ArrayLiteral { elements, .. } => {
                for e in elements {
                    self.analyze_node(e, current_scope, is_condition)?;
                }
            }

            ASTNode::MapLiteral { entries, .. } => {
                for (_, v) in entries {
                    self.analyze_node(v, current_scope, is_condition)?;
                }
            }

            ASTNode::MatchExpr {
                scrutinee,
                arms,
                else_expr,
                ..
            } => {
                self.analyze_node(scrutinee, current_scope, is_condition)?;
                for (_, e) in arms {
                    self.analyze_node(e, current_scope, is_condition)?;
                }
                self.analyze_node(else_expr, current_scope, is_condition)?;
            }

            ASTNode::Lambda { .. } => {}

            ASTNode::Arrow {
                sender, receiver, ..
            } => {
                self.analyze_node(sender, current_scope, is_condition)?;
                self.analyze_node(receiver, current_scope, is_condition)?;
            }

            ASTNode::AwaitExpression { expression, .. }
            | ASTNode::QMarkPropagate { expression, .. } => {
                self.analyze_node(expression, current_scope, is_condition)?;
            }
        }

        Ok(())
    }

    fn record_assignment_target(
        &mut self,
        target: &ASTNode,
        current_scope: ScopeId,
    ) -> Result<(), String> {
        match target {
            ASTNode::Variable { name, .. } => {
                let binding = self.resolve_binding(name).ok_or_else(|| {
                    format!("AstOwnershipAnalyzer: write to undefined var '{}'", name)
                })?;
                self.record_write(binding, current_scope);
            }
            _ => {
                // For complex lvalues (field/index), conservatively treat subexpressions as reads.
                self.analyze_node(target, current_scope, false)?;
            }
        }
        Ok(())
    }

    fn propagate_to_parent(&mut self, child_id: ScopeId) {
        let (parent_id, reads, writes, cond_reads) = {
            let child = &self.scopes[&child_id];
            (
                child.parent,
                child.reads.clone(),
                child.writes.clone(),
                child.condition_reads.clone(),
            )
        };

        if let Some(parent_id) = parent_id {
            let parent = self.scopes.get_mut(&parent_id).unwrap();
            for b in reads {
                if let Some(info) = self.bindings.get(&b) {
                    if info.owner_scope == child_id {
                        continue;
                    }
                }
                parent.reads.insert(b);
            }
            for b in writes {
                if let Some(info) = self.bindings.get(&b) {
                    if info.owner_scope == child_id {
                        continue;
                    }
                }
                parent.writes.insert(b);
            }
            for b in cond_reads {
                if let Some(info) = self.bindings.get(&b) {
                    if info.owner_scope == child_id {
                        continue;
                    }
                }
                parent.condition_reads.insert(b);
            }
        }
    }

    fn build_plans(&self) -> Result<Vec<OwnershipPlan>, String> {
        let mut plans = Vec::new();

        for (_, scope) in &self.scopes {
            if scope.kind != ScopeKind::Loop && scope.kind != ScopeKind::Function {
                continue;
            }
            plans.push(self.build_plan_for_scope(scope.id)?);
        }

        Ok(plans)
    }

    fn relay_path_to_owner(
        &self,
        from_scope: ScopeId,
        owner_scope: ScopeId,
    ) -> Result<Vec<ScopeId>, String> {
        let mut current = from_scope;
        let mut path = Vec::new();

        while current != owner_scope {
            let scope = self
                .scopes
                .get(&current)
                .ok_or_else(|| format!("AstOwnershipAnalyzer: scope {:?} not found", current))?;
            if scope.kind == ScopeKind::Loop {
                path.push(current);
            }
            current = scope.parent.ok_or_else(|| {
                format!(
                    "AstOwnershipAnalyzer: no parent while searching relay path: from={:?} owner={:?}",
                    from_scope, owner_scope
                )
            })?;
        }

        Ok(path)
    }

    fn resolve_binding(&self, name: &str) -> Option<BindingId> {
        self.env_stack
            .iter()
            .rev()
            .find_map(|frame| frame.get(name).copied())
    }

    fn push_env(&mut self) {
        self.env_stack.push(BTreeMap::new());
    }

    fn pop_env(&mut self) {
        self.env_stack.pop();
    }

    fn declare_binding(&mut self, scope_id: ScopeId, name: &str) -> Result<BindingId, String> {
        let Some(frame) = self.env_stack.last_mut() else {
            return Err(
                "AstOwnershipAnalyzer: internal error: declare_binding with empty env_stack"
                    .to_string(),
            );
        };

        if let Some(existing) = frame.get(name).copied() {
            self.scopes
                .get_mut(&scope_id)
                .ok_or_else(|| format!("AstOwnershipAnalyzer: scope {:?} not found", scope_id))?
                .declared
                .entry(name.to_string())
                .or_insert(existing);
            return Ok(existing);
        }

        let binding = BindingId(self.next_binding_id);
        self.next_binding_id += 1;

        frame.insert(name.to_string(), binding);
        self.bindings.insert(
            binding,
            BindingInfo {
                name: name.to_string(),
                owner_scope: scope_id,
            },
        );
        self.scopes
            .get_mut(&scope_id)
            .ok_or_else(|| format!("AstOwnershipAnalyzer: scope {:?} not found", scope_id))?
            .declared
            .insert(name.to_string(), binding);

        Ok(binding)
    }

    fn record_read(&mut self, binding: BindingId, scope_id: ScopeId, is_condition: bool) {
        let scope = self.scopes.get_mut(&scope_id).expect("scope must exist");
        scope.reads.insert(binding);
        if is_condition {
            scope.condition_reads.insert(binding);
        }
    }

    fn record_write(&mut self, binding: BindingId, scope_id: ScopeId) {
        let scope = self.scopes.get_mut(&scope_id).expect("scope must exist");
        scope.writes.insert(binding);
    }
}

impl Default for AstOwnershipAnalyzer {
    fn default() -> Self {
        Self::new()
    }
}

/// Phase 64: Analyze a single loop (condition + body) with parent context.
///
/// This helper is designed for P3 production integration. It creates a temporary
/// function scope for parent-defined variables, then analyzes the loop scope.
///
/// # Arguments
///
/// * `condition` - Loop condition AST node
/// * `body` - Loop body statements
/// * `parent_defined` - Variables defined in parent scope (function params/locals)
///
/// # Returns
///
/// OwnershipPlan for the loop scope only (not the temporary function scope).
///
/// # Example
///
/// ```ignore
/// // loop(i < 10) { local sum=0; sum=sum+1; i=i+1; }
/// let condition = /* i < 10 */;
/// let body = vec![/* local sum=0; sum=sum+1; i=i+1; */];
/// let parent_defined = vec![];
/// let plan = analyze_loop(&condition, &body, &parent_defined)?;
/// // plan.owned_vars contains: sum (is_written=true), i (is_written=true)
/// ```
#[cfg(feature = "normalized_dev")]
pub fn analyze_loop(
    condition: &ASTNode,
    body: &[ASTNode],
    parent_defined: &[String],
) -> Result<OwnershipPlan, String> {
    let mut analyzer = AstOwnershipAnalyzer::new();

    // Create temporary function scope for parent context
    let parent_scope = analyzer.alloc_scope(ScopeKind::Function, None);
    analyzer.push_env();
    for var in parent_defined {
        analyzer.declare_binding(parent_scope, var)?;
    }

    // Create loop scope
    let loop_scope = analyzer.alloc_scope(ScopeKind::Loop, Some(parent_scope));
    analyzer.push_env();

    // Analyze body statements
    for stmt in body {
        analyzer.analyze_node(stmt, loop_scope, false)?;
    }

    // Analyze condition (with is_condition=true flag)
    analyzer.analyze_node(condition, loop_scope, true)?;

    analyzer.pop_env(); // loop scope
                        // Propagate to parent
    analyzer.propagate_to_parent(loop_scope);

    // Build plan for loop only
    let plan = analyzer.build_plan_for_scope(loop_scope)?;
    analyzer.pop_env(); // parent scope
    Ok(plan)
}

impl AstOwnershipAnalyzer {
    /// Phase 64: Build OwnershipPlan for a specific scope (helper for analyze_loop).
    ///
    /// This is a private helper used by `analyze_loop()` to extract a single
    /// scope's OwnershipPlan without building plans for all scopes.
    #[cfg(feature = "normalized_dev")]
    fn build_plan_for_scope(&self, scope_id: ScopeId) -> Result<OwnershipPlan, String> {
        let scope = self
            .scopes
            .get(&scope_id)
            .ok_or_else(|| format!("Scope {:?} not found", scope_id))?;

        let mut plan = OwnershipPlan::new(scope_id);

        // Collect owned vars
        for (name, binding) in &scope.declared {
            let is_written = scope.writes.contains(binding);
            let is_condition_only = is_written && scope.condition_reads.contains(binding);
            plan.owned_vars.push(ScopeOwnedVar {
                name: name.clone(),
                is_written,
                is_condition_only,
            });
        }

        // Collect relay writes
        for binding in &scope.writes {
            let info = self
                .bindings
                .get(binding)
                .ok_or_else(|| format!("AstOwnershipAnalyzer: unknown binding {:?}", binding))?;
            if info.owner_scope == scope_id {
                continue;
            }
            let relay_path = self.relay_path_to_owner(scope_id, info.owner_scope)?;
            plan.relay_writes.push(RelayVar {
                name: info.name.clone(),
                owner_scope: info.owner_scope,
                relay_path,
            });
        }

        // Collect captures
        for binding in &scope.reads {
            if scope.writes.contains(binding) {
                continue;
            }
            let info = self
                .bindings
                .get(binding)
                .ok_or_else(|| format!("AstOwnershipAnalyzer: unknown binding {:?}", binding))?;
            if info.owner_scope == scope_id {
                continue;
            }
            let captured = CapturedVar {
                name: info.name.clone(),
                owner_scope: info.owner_scope,
            };
            if scope.condition_reads.contains(binding) {
                plan.condition_captures.push(captured.clone());
            }
            plan.captures.push(captured);
        }

        #[cfg(debug_assertions)]
        plan.verify_invariants()?;

        Ok(plan)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ast::{BinaryOperator, LiteralValue, Span};

    fn lit_i(i: i64) -> ASTNode {
        ASTNode::Literal {
            value: LiteralValue::Integer(i),
            span: Span::unknown(),
        }
    }

    fn lit_true() -> ASTNode {
        ASTNode::Literal {
            value: LiteralValue::Bool(true),
            span: Span::unknown(),
        }
    }

    fn var(name: &str) -> ASTNode {
        ASTNode::Variable {
            name: name.to_string(),
            span: Span::unknown(),
        }
    }

    #[test]
    fn shadowing_inner_local_does_not_create_relay_write() {
        // local sum=0 (parent)
        // loop(true) { { local sum=1; sum=sum+1 } break }
        // Expected: loop does NOT relay-write parent sum (inner sum shadows it).
        let condition = lit_true();
        let body = vec![
            ASTNode::Program {
                statements: vec![
                    ASTNode::Local {
                        variables: vec!["sum".to_string()],
                        initial_values: vec![Some(Box::new(lit_i(1)))],
                        span: Span::unknown(),
                    },
                    ASTNode::Assignment {
                        target: Box::new(var("sum")),
                        value: Box::new(ASTNode::BinaryOp {
                            operator: BinaryOperator::Add,
                            left: Box::new(var("sum")),
                            right: Box::new(lit_i(1)),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    },
                ],
                span: Span::unknown(),
            },
            ASTNode::Break {
                span: Span::unknown(),
            },
        ];

        let plan = analyze_loop(&condition, &body, &["sum".to_string()]).unwrap();
        assert!(
            !plan.relay_writes.iter().any(|r| r.name == "sum"),
            "inner shadowed sum must not produce relay_writes: {:?}",
            plan.relay_writes
        );
    }

    #[test]
    fn nested_block_local_is_not_a_loop_carrier() {
        // loop(true) { { local x=0; x=x+1 } break }
        // Expected: nested-block local 'x' does not appear in loop owned_vars.
        let condition = lit_true();
        let body = vec![
            ASTNode::Program {
                statements: vec![
                    ASTNode::Local {
                        variables: vec!["x".to_string()],
                        initial_values: vec![Some(Box::new(lit_i(0)))],
                        span: Span::unknown(),
                    },
                    ASTNode::Assignment {
                        target: Box::new(var("x")),
                        value: Box::new(ASTNode::BinaryOp {
                            operator: BinaryOperator::Add,
                            left: Box::new(var("x")),
                            right: Box::new(lit_i(1)),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    },
                ],
                span: Span::unknown(),
            },
            ASTNode::Break {
                span: Span::unknown(),
            },
        ];

        let plan = analyze_loop(&condition, &body, &[]).unwrap();
        assert!(
            !plan.owned_vars.iter().any(|v| v.name == "x"),
            "nested-block local must not be owned by loop: {:?}",
            plan.owned_vars
        );
        assert!(
            !plan.relay_writes.iter().any(|r| r.name == "x"),
            "nested-block local must not be relayed by loop: {:?}",
            plan.relay_writes
        );
        assert!(
            !plan.captures.iter().any(|c| c.name == "x"),
            "nested-block local must not be captured by loop: {:?}",
            plan.captures
        );
    }

    #[test]
    fn outer_update_is_relay_write_when_not_shadowed() {
        // local sum=0 (parent)
        // loop(true) { sum=sum+1; break }
        // Expected: loop relays write to parent-owned sum.
        let condition = lit_true();
        let body = vec![
            ASTNode::Assignment {
                target: Box::new(var("sum")),
                value: Box::new(ASTNode::BinaryOp {
                    operator: BinaryOperator::Add,
                    left: Box::new(var("sum")),
                    right: Box::new(lit_i(1)),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            },
            ASTNode::Break {
                span: Span::unknown(),
            },
        ];

        let plan = analyze_loop(&condition, &body, &["sum".to_string()]).unwrap();
        let relay = plan
            .relay_writes
            .iter()
            .find(|r| r.name == "sum")
            .expect("expected relay write for sum");
        assert_ne!(relay.owner_scope, plan.scope_id);
        assert_eq!(relay.relay_path.len(), 1);
        assert_eq!(relay.relay_path[0], plan.scope_id);
    }

    #[test]
    fn loop_local_carrier_is_owned_and_written() {
        // function main() {
        //   loop(true) { local sum=0; sum = sum + 1; break }
        // }
        let ast = ASTNode::FunctionDeclaration {
            name: "main".to_string(),
            params: vec![],
            body: vec![ASTNode::Loop {
                condition: Box::new(lit_true()),
                body: vec![
                    ASTNode::Local {
                        variables: vec!["sum".to_string()],
                        initial_values: vec![Some(Box::new(lit_i(0)))],
                        span: Span::unknown(),
                    },
                    ASTNode::Assignment {
                        target: Box::new(var("sum")),
                        value: Box::new(ASTNode::BinaryOp {
                            operator: BinaryOperator::Add,
                            left: Box::new(var("sum")),
                            right: Box::new(lit_i(1)),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    },
                    ASTNode::Break {
                        span: Span::unknown(),
                    },
                ],
                span: Span::unknown(),
            }],
            is_static: false,
            is_override: false,
            span: Span::unknown(),
        };

        let mut analyzer = AstOwnershipAnalyzer::new();
        let plans = analyzer.analyze_ast(&ast).unwrap();

        let loop_plans: Vec<_> = plans
            .iter()
            .filter(|p| p.owned_vars.iter().any(|v| v.name == "sum"))
            .collect();
        assert_eq!(loop_plans.len(), 1);
        let plan = loop_plans[0];
        let sum = plan.owned_vars.iter().find(|v| v.name == "sum").unwrap();
        assert!(sum.is_written);
    }

    #[test]
    fn condition_capture_is_reported_for_loop() {
        // local limit=10;
        // loop(i < limit) { local i=0; i=i+1; break }
        let ast = ASTNode::FunctionDeclaration {
            name: "main".to_string(),
            params: vec![],
            body: vec![
                ASTNode::Local {
                    variables: vec!["limit".to_string()],
                    initial_values: vec![Some(Box::new(lit_i(10)))],
                    span: Span::unknown(),
                },
                ASTNode::Loop {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Less,
                        left: Box::new(var("i")),
                        right: Box::new(var("limit")),
                        span: Span::unknown(),
                    }),
                    body: vec![
                        ASTNode::Local {
                            variables: vec!["i".to_string()],
                            initial_values: vec![Some(Box::new(lit_i(0)))],
                            span: Span::unknown(),
                        },
                        ASTNode::Assignment {
                            target: Box::new(var("i")),
                            value: Box::new(ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(var("i")),
                                right: Box::new(lit_i(1)),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        },
                        ASTNode::Break {
                            span: Span::unknown(),
                        },
                    ],
                    span: Span::unknown(),
                },
            ],
            is_static: false,
            is_override: false,
            span: Span::unknown(),
        };

        let mut analyzer = AstOwnershipAnalyzer::new();
        let plans = analyzer.analyze_ast(&ast).unwrap();

        let loop_plan = plans
            .iter()
            .find(|p| p.condition_captures.iter().any(|c| c.name == "limit"))
            .expect("expected a loop plan capturing limit");

        assert!(loop_plan.captures.iter().any(|c| c.name == "limit"));
        assert!(loop_plan
            .condition_captures
            .iter()
            .any(|c| c.name == "limit"));
    }

    #[test]
    fn relay_write_detected_for_outer_owned_var() {
        // local sum=0;
        // loop(true) { sum=sum+1; break }
        let ast = ASTNode::FunctionDeclaration {
            name: "main".to_string(),
            params: vec![],
            body: vec![
                ASTNode::Local {
                    variables: vec!["sum".to_string()],
                    initial_values: vec![Some(Box::new(lit_i(0)))],
                    span: Span::unknown(),
                },
                ASTNode::Loop {
                    condition: Box::new(lit_true()),
                    body: vec![
                        ASTNode::Assignment {
                            target: Box::new(var("sum")),
                            value: Box::new(ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(var("sum")),
                                right: Box::new(lit_i(1)),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        },
                        ASTNode::Break {
                            span: Span::unknown(),
                        },
                    ],
                    span: Span::unknown(),
                },
            ],
            is_static: false,
            is_override: false,
            span: Span::unknown(),
        };

        let mut analyzer = AstOwnershipAnalyzer::new();
        let plans = analyzer.analyze_ast(&ast).unwrap();

        let loop_plan = plans
            .iter()
            .find(|p| p.relay_writes.iter().any(|r| r.name == "sum"))
            .expect("expected a loop plan with relay write");

        let relay = loop_plan
            .relay_writes
            .iter()
            .find(|r| r.name == "sum")
            .unwrap();
        assert_ne!(relay.owner_scope, loop_plan.scope_id);
        assert_eq!(relay.relay_path.len(), 1);
        assert_eq!(relay.relay_path[0], loop_plan.scope_id);
    }

    /// Phase 66: Test multihop relay (3-layer nested loops)
    ///
    /// Structure:
    /// ```text
    /// loop(L1) {
    ///   local sum = 0;        // L1 owns sum
    ///   loop(L2) {
    ///     loop(L3) {
    ///       sum = sum + 1;    // L3 writes to sum (owned by L1)
    ///     }
    ///   }
    /// }
    /// ```
    ///
    /// Expected: L3's relay_writes has `sum` with relay_path.len() == 2 (L3 → L2 → owner L1)
    #[test]
    fn multihop_relay_detected_for_3_layer_nested_loops() {
        let ast = ASTNode::FunctionDeclaration {
            name: "main".to_string(),
            params: vec![],
            body: vec![
                // L1: outermost loop (owns sum)
                ASTNode::Loop {
                    condition: Box::new(lit_true()),
                    body: vec![
                        // local sum = 0  (L1 owns)
                        ASTNode::Local {
                            variables: vec!["sum".to_string()],
                            initial_values: vec![Some(Box::new(lit_i(0)))],
                            span: Span::unknown(),
                        },
                        // L2: middle loop
                        ASTNode::Loop {
                            condition: Box::new(lit_true()),
                            body: vec![
                                // L3: innermost loop
                                ASTNode::Loop {
                                    condition: Box::new(lit_true()),
                                    body: vec![
                                        // sum = sum + 1 (L3 writes to L1-owned)
                                        ASTNode::Assignment {
                                            target: Box::new(var("sum")),
                                            value: Box::new(ASTNode::BinaryOp {
                                                operator: BinaryOperator::Add,
                                                left: Box::new(var("sum")),
                                                right: Box::new(lit_i(1)),
                                                span: Span::unknown(),
                                            }),
                                            span: Span::unknown(),
                                        },
                                        ASTNode::Break {
                                            span: Span::unknown(),
                                        },
                                    ],
                                    span: Span::unknown(),
                                },
                                ASTNode::Break {
                                    span: Span::unknown(),
                                },
                            ],
                            span: Span::unknown(),
                        },
                        ASTNode::Break {
                            span: Span::unknown(),
                        },
                    ],
                    span: Span::unknown(),
                },
            ],
            is_static: false,
            is_override: false,
            span: Span::unknown(),
        };

        let mut analyzer = AstOwnershipAnalyzer::new();
        let plans = analyzer.analyze_ast(&ast).unwrap();

        // Find L1 (owns sum)
        let l1_plan = plans
            .iter()
            .find(|p| p.owned_vars.iter().any(|v| v.name == "sum"))
            .expect("expected L1 plan with owned sum");
        let l1_scope_id = l1_plan.scope_id;

        // Find L3 (writes to sum via relay) - must be the innermost loop (largest scope_id among relays)
        // Note: Both L2 and L3 have relay_writes to sum, but L3 has longer relay_path
        let relay_plans: Vec<_> = plans
            .iter()
            .filter(|p| p.relay_writes.iter().any(|r| r.name == "sum") && p.scope_id != l1_scope_id)
            .collect();

        // Get the innermost one (highest scope_id = L3)
        let l3_plan = relay_plans
            .iter()
            .max_by_key(|p| p.scope_id.0)
            .expect("expected L3 plan with relay write to sum");

        let relay = l3_plan
            .relay_writes
            .iter()
            .find(|r| r.name == "sum")
            .expect("expected sum in relay_writes");

        // Verify: relay_path has 2 hops (L3 → L2)
        // Note: relay_path does NOT include owner (L1), only intermediate loops
        assert_eq!(
            relay.relay_path.len(),
            2,
            "multihop relay should have 2 hops: L3 (this scope) → L2. Got {:?}",
            relay.relay_path
        );

        // Verify: relay_path[0] == L3's scope_id (Phase 66 invariant)
        assert_eq!(
            relay.relay_path[0], l3_plan.scope_id,
            "relay_path[0] must be this scope (L3)"
        );

        // Verify: owner is L1
        assert_eq!(relay.owner_scope, l1_scope_id, "owner_scope must be L1");
    }
}
