use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span, UnaryOperator};
use crate::mir::control_tree::step_tree_contract_box::{
    StepTreeContract, StepTreeContractBox,
};
use crate::mir::control_tree::step_tree_facts::StepTreeFacts;

use std::collections::BTreeSet;

#[derive(Debug, Clone, PartialEq)]
pub struct StepTree {
    pub root: StepNode,
    pub features: StepTreeFeatures,
    pub contract: StepTreeContract,
    pub signature: StepTreeSignature,
}

#[derive(Debug, Clone, PartialEq, Default)]
pub struct StepTreeFeatures {
    pub has_if: bool,
    pub has_loop: bool,
    pub has_break: bool,
    pub has_continue: bool,
    pub has_return: bool,
    pub max_if_depth: u32,
    pub max_loop_depth: u32,
}

#[derive(Debug, Clone, PartialEq)]
pub enum StepNode {
    Block(Vec<StepNode>),
    If {
        cond: AstSummary,
        cond_ast: AstNodeHandle,
        then_branch: Box<StepNode>,
        else_branch: Option<Box<StepNode>>,
        span: Span,
    },
    Loop {
        cond: AstSummary,
        cond_ast: AstNodeHandle,
        body: Box<StepNode>,
        span: Span,
    },
    Stmt { kind: StepStmtKind, span: Span },
}

/// AST 参照の軽量ハンドル（Phase 119: dev-only 観測用）
///
/// SSOT: cond は AST 参照を保持する。
/// - 将来的に AstExprId 等に移行可能。
/// - Phase 119 では Clone を持つ Box<ASTNode> で実装（dev-only なので許容）。
#[derive(Debug, Clone, PartialEq)]
pub struct AstNodeHandle(pub Box<ASTNode>);

#[derive(Debug, Clone, PartialEq)]
pub enum StepStmtKind {
    LocalDecl { vars: Vec<String> },
    Assign {
        target: Option<String>,
        /// Phase 128: assignment value AST (for Normalized lowering)
        value_ast: Option<AstNodeHandle>,
    },
    Print,
    Return {
        /// Phase 123: return value AST (for Normalized lowering)
        value_ast: Option<AstNodeHandle>,
    },
    Break,
    Continue,
    Other(&'static str),
}

#[derive(Debug, Clone, PartialEq)]
pub enum AstSummary {
    Variable(String),
    Literal(LiteralValue),
    Unary {
        op: UnaryOperator,
        expr: Box<AstSummary>,
    },
    Binary {
        op: BinaryOperator,
        lhs: Box<AstSummary>,
        rhs: Box<AstSummary>,
    },
    Other(&'static str),
}

impl StepTree {
    pub fn to_compact_string(&self) -> String {
        let mut out = String::new();
        self.root.write_compact(&mut out, 0);
        out
    }

    pub fn signature_basis_string(&self) -> String {
        let mut kinds = Vec::new();
        collect_node_kinds(&self.root, &mut kinds);
        let kinds = kinds.join(",");
        self.contract.signature_basis_string(&kinds)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ExitKind {
    Return,
    Break,
    Continue,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum StepCapability {
    If,
    Loop,
    NestedIf,
    NestedLoop,
    Return,
    Break,
    Continue,
    TryCatch,
    Throw,
    Lambda,
    While,
    ForRange,
    Match,
    Arrow,
}

// StepTreeContract moved to step_tree_contract_box.rs (Phase 120)

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StepTreeSignature(pub u64);

impl StepTreeSignature {
    pub fn from_basis_string(basis: &str) -> Self {
        // FNV-1a 64-bit (stable, no external deps).
        let mut hash: u64 = 0xcbf29ce484222325;
        for b in basis.as_bytes() {
            hash ^= *b as u64;
            hash = hash.wrapping_mul(0x100000001b3);
        }
        StepTreeSignature(hash)
    }

    pub fn to_hex(self) -> String {
        format!("{:016x}", self.0)
    }
}

impl StepNode {
    fn write_compact(&self, out: &mut String, indent: usize) {
        let pad = "  ".repeat(indent);
        match self {
            StepNode::Block(nodes) => {
                out.push_str(&format!("{pad}Block(len={})\n", nodes.len()));
                for n in nodes {
                    n.write_compact(out, indent + 1);
                }
            }
            StepNode::If {
                cond,
                then_branch,
                else_branch,
                ..
            } => {
                out.push_str(&format!(
                    "{pad}If(cond={})\n",
                    cond.to_compact_string()
                ));
                out.push_str(&format!("{pad}  then:\n"));
                then_branch.write_compact(out, indent + 2);
                if let Some(else_branch) = else_branch {
                    out.push_str(&format!("{pad}  else:\n"));
                    else_branch.write_compact(out, indent + 2);
                }
            }
            StepNode::Loop { cond, body, .. } => {
                out.push_str(&format!(
                    "{pad}Loop(cond={})\n",
                    cond.to_compact_string()
                ));
                body.write_compact(out, indent + 1);
            }
            StepNode::Stmt { kind, .. } => {
                out.push_str(&format!("{pad}Stmt({})\n", kind.to_compact_string()));
            }
        }
    }
}

impl StepStmtKind {
    fn to_compact_string(&self) -> String {
        match self {
            StepStmtKind::LocalDecl { vars } => format!("local({})", vars.join(",")),
            StepStmtKind::Assign { target, .. } => match target {
                Some(name) => format!("assign({name})"),
                None => "assign(?)".to_string(),
            },
            StepStmtKind::Print => "print".to_string(),
            StepStmtKind::Return { value_ast } => {
                if value_ast.is_some() {
                    "return(value)".to_string()
                } else {
                    "return(void)".to_string()
                }
            }
            StepStmtKind::Break => "break".to_string(),
            StepStmtKind::Continue => "continue".to_string(),
            StepStmtKind::Other(name) => format!("other:{name}"),
        }
    }
}

impl AstSummary {
    fn to_compact_string(&self) -> String {
        match self {
            AstSummary::Variable(name) => format!("var:{name}"),
            AstSummary::Literal(lit) => format!("lit:{}", lit_to_sig_string(lit)),
            AstSummary::Unary { op, expr } => format!("({op:?} {})", expr.to_compact_string()),
            AstSummary::Binary { op, lhs, rhs } => format!(
                "({} {} {})",
                lhs.to_compact_string(),
                op,
                rhs.to_compact_string()
            ),
            AstSummary::Other(k) => format!("other:{k}"),
        }
    }
}

fn lit_to_sig_string(lit: &LiteralValue) -> String {
    match lit {
        LiteralValue::String(s) => format!("str:{}", escape_sig_atom(s)),
        LiteralValue::Integer(i) => format!("int:{i}"),
        LiteralValue::Float(f) => format!("float:{:016x}", f.to_bits()),
        LiteralValue::Bool(b) => format!("bool:{}", if *b { 1 } else { 0 }),
        LiteralValue::Null => "null".to_string(),
        LiteralValue::Void => "void".to_string(),
    }
}

fn escape_sig_atom(s: &str) -> String {
    // Minimal stable escaping for signature strings.
    s.replace('\\', "\\\\").replace('|', "\\|").replace(',', "\\,")
}

pub struct StepTreeBuilderBox;

impl StepTreeBuilderBox {
    pub fn build_from_ast(ast: &ASTNode) -> StepTree {
        match ast {
            ASTNode::Program { statements, .. } => Self::build_from_block(statements),
            ASTNode::ScopeBox { body, .. } => Self::build_from_block(body),
            _ => {
                let (node, features) = Self::build_node(ast, 0, 0);
                build_step_tree(node, features)
            }
        }
    }

    pub fn build_from_block(stmts: &[ASTNode]) -> StepTree {
        let mut nodes = Vec::with_capacity(stmts.len());
        let mut features = StepTreeFeatures::default();
        for stmt in stmts {
            let (node, node_features) = Self::build_node(stmt, 0, 0);
            nodes.push(node);
            features = merge_features(features, node_features);
        }
        build_step_tree(StepNode::Block(nodes), features)
    }

    fn build_node(ast: &ASTNode, if_depth: u32, loop_depth: u32) -> (StepNode, StepTreeFeatures) {
        match ast {
            ASTNode::If {
                condition,
                then_body,
                else_body,
                span,
            } => {
                let cond = summarize_ast(condition);
                let cond_ast = AstNodeHandle(condition.clone());
                let (then_node, then_features) =
                    Self::build_block_node(then_body, if_depth + 1, loop_depth);
                let (else_node, else_features) = match else_body {
                    Some(else_body) => {
                        let (node, f) =
                            Self::build_block_node(else_body, if_depth + 1, loop_depth);
                        (Some(Box::new(node)), f)
                    }
                    None => (None, StepTreeFeatures::default()),
                };
                let mut features = StepTreeFeatures {
                    has_if: true,
                    max_if_depth: (if_depth + 1).max(then_features.max_if_depth),
                    ..StepTreeFeatures::default()
                };
                features = merge_features(features, then_features);
                features = merge_features(features, else_features);

                (
                    StepNode::If {
                        cond,
                        cond_ast,
                        then_branch: Box::new(then_node),
                        else_branch: else_node,
                        span: span.clone(),
                    },
                    features,
                )
            }
            ASTNode::Loop {
                condition, body, span, ..
            } => {
                let cond = summarize_ast(condition);
                let cond_ast = AstNodeHandle(condition.clone());
                let (body_node, body_features) =
                    Self::build_block_node(body, if_depth, loop_depth + 1);
                let mut features = StepTreeFeatures {
                    has_loop: true,
                    max_loop_depth: (loop_depth + 1).max(body_features.max_loop_depth),
                    ..StepTreeFeatures::default()
                };
                features = merge_features(features, body_features);
                (
                    StepNode::Loop {
                        cond,
                        cond_ast,
                        body: Box::new(body_node),
                        span: span.clone(),
                    },
                    features,
                )
            }
            ASTNode::ScopeBox { body, span } => {
                let (node, features) = Self::build_block_node(body, if_depth, loop_depth);
                (node.with_span(span.clone()), features)
            }
            ASTNode::Return { value, span } => (
                StepNode::Stmt {
                    kind: StepStmtKind::Return {
                        value_ast: value.as_ref().map(|v| AstNodeHandle(v.clone())),
                    },
                    span: span.clone(),
                },
                StepTreeFeatures {
                    has_return: true,
                    ..StepTreeFeatures::default()
                },
            ),
            ASTNode::Break { span } => (
                StepNode::Stmt {
                    kind: StepStmtKind::Break,
                    span: span.clone(),
                },
                StepTreeFeatures {
                    has_break: true,
                    ..StepTreeFeatures::default()
                },
            ),
            ASTNode::Continue { span } => (
                StepNode::Stmt {
                    kind: StepStmtKind::Continue,
                    span: span.clone(),
                },
                StepTreeFeatures {
                    has_continue: true,
                    ..StepTreeFeatures::default()
                },
            ),
            ASTNode::Local {
                variables, span, ..
            } => (
                StepNode::Stmt {
                    kind: StepStmtKind::LocalDecl {
                        vars: variables.clone(),
                    },
                    span: span.clone(),
                },
                StepTreeFeatures::default(),
            ),
            ASTNode::Assignment { span, value, .. } => (
                StepNode::Stmt {
                    kind: StepStmtKind::Assign {
                        target: match ast {
                            ASTNode::Assignment { target, .. } => match target.as_ref() {
                                ASTNode::Variable { name, .. } => Some(name.clone()),
                                _ => None,
                            },
                            _ => None,
                        },
                        /// Phase 128: Store value AST for Normalized lowering
                        value_ast: Some(AstNodeHandle(value.clone())),
                    },
                    span: span.clone(),
                },
                StepTreeFeatures::default(),
            ),
            ASTNode::Print { span, .. } => (
                StepNode::Stmt {
                    kind: StepStmtKind::Print,
                    span: span.clone(),
                },
                StepTreeFeatures::default(),
            ),
            other => (
                StepNode::Stmt {
                    kind: StepStmtKind::Other(ast_kind_name(other)),
                    span: other.span(),
                },
                StepTreeFeatures::default(),
            ),
        }
    }

    fn build_block_node(
        stmts: &[ASTNode],
        if_depth: u32,
        loop_depth: u32,
    ) -> (StepNode, StepTreeFeatures) {
        let mut nodes = Vec::with_capacity(stmts.len());
        let mut features = StepTreeFeatures::default();
        for stmt in stmts {
            let (node, node_features) = Self::build_node(stmt, if_depth, loop_depth);
            nodes.push(node);
            features = merge_features(features, node_features);
        }
        (StepNode::Block(nodes), features)
    }
}

fn build_step_tree(root: StepNode, features: StepTreeFeatures) -> StepTree {
    // Phase 120: Facts → Contract → Signature (separated concerns)
    let facts = extract_facts_from_tree(&root, &features);
    let contract = StepTreeContractBox::from_facts(&facts);
    let mut kinds = Vec::new();
    collect_node_kinds(&root, &mut kinds);
    let kinds = kinds.join(",");
    let basis = contract.signature_basis_string(&kinds);
    let signature = StepTreeSignature::from_basis_string(&basis);

    StepTree {
        root,
        features,
        contract,
        signature,
    }
}

/// Extract raw facts from StepNode tree (Phase 120)
fn extract_facts_from_tree(root: &StepNode, features: &StepTreeFeatures) -> StepTreeFacts {
    let mut facts = StepTreeFacts::new();

    // Required caps from features (structural only)
    if features.has_if {
        facts.add_capability(StepCapability::If);
    }
    if features.max_if_depth > 1 {
        facts.add_capability(StepCapability::NestedIf);
    }
    if features.has_loop {
        facts.add_capability(StepCapability::Loop);
    }
    if features.max_loop_depth > 1 {
        facts.add_capability(StepCapability::NestedLoop);
    }
    if features.has_return {
        facts.add_capability(StepCapability::Return);
    }
    if features.has_break {
        facts.add_capability(StepCapability::Break);
    }
    if features.has_continue {
        facts.add_capability(StepCapability::Continue);
    }

    walk_for_facts(root, &mut facts);
    facts
}

/// Walk StepNode tree to collect facts (Phase 120, Phase 124)
fn walk_for_facts(node: &StepNode, facts: &mut StepTreeFacts) {
    match node {
        StepNode::Block(nodes) => {
            for n in nodes {
                walk_for_facts(n, facts);
            }
        }
        StepNode::If {
            cond,
            cond_ast,
            then_branch,
            else_branch,
            ..
        } => {
            facts.add_cond_sig(cond.to_compact_string());
            // Phase 124: Extract reads from condition AST
            extract_variables_from_ast(&cond_ast.0, facts);
            walk_for_facts(then_branch, facts);
            if let Some(else_branch) = else_branch {
                walk_for_facts(else_branch, facts);
            }
        }
        StepNode::Loop { cond, cond_ast, body, .. } => {
            facts.add_cond_sig(cond.to_compact_string());
            // Phase 124: Extract reads from condition AST
            extract_variables_from_ast(&cond_ast.0, facts);
            walk_for_facts(body, facts);
        }
        StepNode::Stmt { kind, .. } => {
            match kind {
                StepStmtKind::LocalDecl { vars } => {
                    for v in vars {
                        facts.add_write(v.clone());
                    }
                }
                StepStmtKind::Assign { target, value_ast } => {
                    if let Some(name) = target.as_ref() {
                        facts.add_write(name.clone());
                    }
                    // Phase 128: Extract reads from assignment value AST
                    if let Some(ast) = value_ast {
                        extract_variables_from_ast(&ast.0, facts);
                    }
                }
                StepStmtKind::Print => {}
                StepStmtKind::Return { value_ast } => {
                    facts.add_exit(ExitKind::Return);
                    // Phase 124: Extract reads from return value AST
                    if let Some(ast) = value_ast {
                        extract_variables_from_ast(&ast.0, facts);
                    }
                }
                StepStmtKind::Break => {
                    facts.add_exit(ExitKind::Break);
                }
                StepStmtKind::Continue => {
                    facts.add_exit(ExitKind::Continue);
                }
                StepStmtKind::Other(name) => match *name {
                    "TryCatch" => {
                        facts.add_capability(StepCapability::TryCatch);
                    }
                    "Throw" => {
                        facts.add_capability(StepCapability::Throw);
                    }
                    "Lambda" => {
                        facts.add_capability(StepCapability::Lambda);
                    }
                    "While" => {
                        facts.add_capability(StepCapability::While);
                    }
                    "ForRange" => {
                        facts.add_capability(StepCapability::ForRange);
                    }
                    "MatchExpr" => {
                        facts.add_capability(StepCapability::Match);
                    }
                    "Arrow" => {
                        facts.add_capability(StepCapability::Arrow);
                    }
                    _ => {}
                },
            }
        }
    }
}

/// Extract Variable names from AST (Phase 124: reads collection)
///
/// SSOT for reads extraction:
/// - Recursively walk AST tree
/// - Add Variable { name } to facts.reads
/// - Ignore other node types
fn extract_variables_from_ast(ast: &ASTNode, facts: &mut StepTreeFacts) {
    match ast {
        ASTNode::Variable { name, .. } => {
            facts.add_read(name.clone());
        }
        // Recursively walk binary/unary operations
        ASTNode::BinaryOp { left, right, .. } => {
            extract_variables_from_ast(left, facts);
            extract_variables_from_ast(right, facts);
        }
        ASTNode::UnaryOp { operand, .. } => {
            extract_variables_from_ast(operand, facts);
        }
        // Function calls
        ASTNode::FunctionCall { arguments, .. } => {
            for arg in arguments {
                extract_variables_from_ast(arg, facts);
            }
        }
        // Method calls
        ASTNode::MethodCall { object, arguments, .. } => {
            extract_variables_from_ast(object, facts);
            for arg in arguments {
                extract_variables_from_ast(arg, facts);
            }
        }
        // Field access
        ASTNode::FieldAccess { object, .. } => {
            extract_variables_from_ast(object, facts);
        }
        // Array/Index access
        ASTNode::Index { target, index, .. } => {
            extract_variables_from_ast(target, facts);
            extract_variables_from_ast(index, facts);
        }
        // Assignment (RHS only)
        ASTNode::Assignment { value, .. } => {
            extract_variables_from_ast(value, facts);
        }
        // Print
        ASTNode::Print { expression, .. } => {
            extract_variables_from_ast(expression, facts);
        }
        // Ignore literals, keywords, and other non-variable nodes
        _ => {}
    }
}

fn merge_features(mut a: StepTreeFeatures, b: StepTreeFeatures) -> StepTreeFeatures {
    a.has_if |= b.has_if;
    a.has_loop |= b.has_loop;
    a.has_break |= b.has_break;
    a.has_continue |= b.has_continue;
    a.has_return |= b.has_return;
    a.max_if_depth = a.max_if_depth.max(b.max_if_depth);
    a.max_loop_depth = a.max_loop_depth.max(b.max_loop_depth);
    a
}

// StepTreeContractBox moved to step_tree_contract_box.rs (Phase 120)
// extract_facts_from_tree + walk_for_facts replace the old compute/walk pattern

fn collect_node_kinds(node: &StepNode, out: &mut Vec<String>) {
    match node {
        StepNode::Block(nodes) => {
            out.push("Block".to_string());
            for n in nodes {
                collect_node_kinds(n, out);
            }
        }
        StepNode::If {
            then_branch,
            else_branch,
            ..
        } => {
            out.push("If".to_string());
            collect_node_kinds(then_branch, out);
            if let Some(else_branch) = else_branch {
                collect_node_kinds(else_branch, out);
            }
        }
        StepNode::Loop { body, .. } => {
            out.push("Loop".to_string());
            collect_node_kinds(body, out);
        }
        StepNode::Stmt { kind, .. } => {
            out.push(format!("Stmt({})", kind.to_compact_string()));
        }
    }
}

fn summarize_ast(ast: &ASTNode) -> AstSummary {
    match ast {
        ASTNode::Variable { name, .. } => AstSummary::Variable(name.clone()),
        ASTNode::Literal { value, .. } => AstSummary::Literal(value.clone()),
        ASTNode::UnaryOp {
            operator, operand, ..
        } => AstSummary::Unary {
            op: operator.clone(),
            expr: Box::new(summarize_ast(operand)),
        },
        ASTNode::BinaryOp {
            operator,
            left,
            right,
            ..
        } => AstSummary::Binary {
            op: operator.clone(),
            lhs: Box::new(summarize_ast(left)),
            rhs: Box::new(summarize_ast(right)),
        },
        other => AstSummary::Other(ast_kind_name(other)),
    }
}

fn ast_kind_name(ast: &ASTNode) -> &'static str {
    match ast {
        ASTNode::Program { .. } => "Program",
        ASTNode::Assignment { .. } => "Assignment",
        ASTNode::Print { .. } => "Print",
        ASTNode::If { .. } => "If",
        ASTNode::Loop { .. } => "Loop",
        ASTNode::While { .. } => "While",
        ASTNode::ForRange { .. } => "ForRange",
        ASTNode::Return { .. } => "Return",
        ASTNode::Break { .. } => "Break",
        ASTNode::Continue { .. } => "Continue",
        ASTNode::UsingStatement { .. } => "UsingStatement",
        ASTNode::ImportStatement { .. } => "ImportStatement",
        ASTNode::Nowait { .. } => "Nowait",
        ASTNode::AwaitExpression { .. } => "AwaitExpression",
        ASTNode::QMarkPropagate { .. } => "QMarkPropagate",
        ASTNode::MatchExpr { .. } => "MatchExpr",
        ASTNode::ArrayLiteral { .. } => "ArrayLiteral",
        ASTNode::MapLiteral { .. } => "MapLiteral",
        ASTNode::Lambda { .. } => "Lambda",
        ASTNode::Arrow { .. } => "Arrow",
        ASTNode::TryCatch { .. } => "TryCatch",
        ASTNode::Throw { .. } => "Throw",
        ASTNode::BoxDeclaration { .. } => "BoxDeclaration",
        ASTNode::FunctionDeclaration { .. } => "FunctionDeclaration",
        ASTNode::GlobalVar { .. } => "GlobalVar",
        ASTNode::Literal { .. } => "Literal",
        ASTNode::Variable { .. } => "Variable",
        ASTNode::UnaryOp { .. } => "UnaryOp",
        ASTNode::BinaryOp { .. } => "BinaryOp",
        ASTNode::GroupedAssignmentExpr { .. } => "GroupedAssignmentExpr",
        ASTNode::MethodCall { .. } => "MethodCall",
        ASTNode::Call { .. } => "Call",
        ASTNode::FunctionCall { .. } => "FunctionCall",
        ASTNode::FieldAccess { .. } => "FieldAccess",
        ASTNode::Index { .. } => "Index",
        ASTNode::New { .. } => "New",
        ASTNode::This { .. } => "This",
        ASTNode::Me { .. } => "Me",
        ASTNode::FromCall { .. } => "FromCall",
        ASTNode::ThisField { .. } => "ThisField",
        ASTNode::MeField { .. } => "MeField",
        ASTNode::Local { .. } => "Local",
        ASTNode::ScopeBox { .. } => "ScopeBox",
        ASTNode::Outbox { .. } => "Outbox",
    }
}

impl StepNode {
    fn with_span(self, span: Span) -> StepNode {
        match self {
            StepNode::Block(nodes) => StepNode::Block(nodes),
            StepNode::If {
                cond,
                cond_ast,
                then_branch,
                else_branch,
                ..
            } => StepNode::If {
                cond,
                cond_ast,
                then_branch,
                else_branch,
                span,
            },
            StepNode::Loop { cond, cond_ast, body, .. } => StepNode::Loop { cond, cond_ast, body, span },
            StepNode::Stmt { kind, .. } => StepNode::Stmt { kind, span },
        }
    }
}

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

    fn str_lit(s: &str) -> ASTNode {
        ASTNode::Literal {
            value: LiteralValue::String(s.to_string()),
            span: Span::unknown(),
        }
    }

    fn eq(a: ASTNode, b: ASTNode) -> ASTNode {
        ASTNode::BinaryOp {
            operator: BinaryOperator::Equal,
            left: Box::new(a),
            right: Box::new(b),
            span: Span::unknown(),
        }
    }

    fn assign_x(num: i64) -> ASTNode {
        ASTNode::Assignment {
            target: Box::new(ASTNode::Variable {
                name: "x".to_string(),
                span: Span::unknown(),
            }),
            value: Box::new(ASTNode::Literal {
                value: LiteralValue::Integer(num),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        }
    }

    #[test]
    fn build_step_tree_if_only_nested_if_is_structural() {
        // Equivalent shape to Phase103 "if-only merge" fixture:
        //
        // local x = 0
        // if "x" == "x" { if "y" == "z" { x=1 } else { x=2 } } else { x=3 }
        // print(x)
        let ast = vec![
            ASTNode::Local {
                variables: vec!["x".to_string()],
                initial_values: vec![Some(Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(0),
                    span: Span::unknown(),
                }))],
                span: Span::unknown(),
            },
            ASTNode::If {
                condition: Box::new(eq(str_lit("x"), str_lit("x"))),
                then_body: vec![ASTNode::If {
                    condition: Box::new(eq(str_lit("y"), str_lit("z"))),
                    then_body: vec![assign_x(1)],
                    else_body: Some(vec![assign_x(2)]),
                    span: Span::unknown(),
                }],
                else_body: Some(vec![assign_x(3)]),
                span: Span::unknown(),
            },
            ASTNode::Print {
                expression: Box::new(ASTNode::Variable {
                    name: "x".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            },
        ];

        let tree = StepTreeBuilderBox::build_from_block(&ast);
        assert!(tree.features.has_if);
        assert!(!tree.features.has_loop);
        assert_eq!(tree.features.max_if_depth, 2);
        assert_eq!(tree.contract.exits.len(), 0);
        assert!(tree.contract.writes.contains("x"));
        assert!(tree.contract.required_caps.contains(&StepCapability::If));
        assert!(tree.contract.required_caps.contains(&StepCapability::NestedIf));

        let basis = tree.signature_basis_string();
        assert_eq!(
            basis,
            "kinds=Block,Stmt(local(x)),If,Block,If,Block,Stmt(assign(x)),Block,Stmt(assign(x)),Block,Stmt(assign(x)),Stmt(print);exits=;writes=x;reads=;caps=If,NestedIf;conds=(lit:str:x == lit:str:x)|(lit:str:y == lit:str:z)"
        );

        let tree2 = StepTreeBuilderBox::build_from_block(&ast);
        assert_eq!(tree.signature, tree2.signature);

        match tree.root {
            StepNode::Block(nodes) => {
                assert_eq!(nodes.len(), 3);
                match &nodes[1] {
                    StepNode::If { then_branch, cond_ast, .. } => {
                        // cond_ast should be populated
                        assert!(matches!(&cond_ast.0.as_ref(), ASTNode::BinaryOp { .. }));

                        match &**then_branch {
                            StepNode::Block(inner_nodes) => match &inner_nodes[0] {
                                StepNode::If { cond_ast: inner_cond_ast, .. } => {
                                    // inner cond_ast should also be populated
                                    assert!(matches!(&inner_cond_ast.0.as_ref(), ASTNode::BinaryOp { .. }));
                                }
                                other => panic!("expected nested If, got {other:?}"),
                            },
                            other => panic!("expected Block in then_branch, got {other:?}"),
                        }
                    }
                    other => panic!("expected If at index 1, got {other:?}"),
                }
            }
            other => panic!("expected root Block, got {other:?}"),
        }
    }

    #[test]
    fn step_tree_cond_ast_is_populated() {
        // Phase 119: cond_ast should hold AST reference.
        let ast = vec![ASTNode::If {
            condition: Box::new(eq(str_lit("a"), str_lit("b"))),
            then_body: vec![assign_x(1)],
            else_body: None,
            span: Span::unknown(),
        }];

        let tree = StepTreeBuilderBox::build_from_block(&ast);

        match &tree.root {
            StepNode::Block(nodes) => match &nodes[0] {
                StepNode::If { cond_ast, .. } => {
                    // cond_ast should be populated with BinaryOp
                    assert!(matches!(&cond_ast.0.as_ref(), ASTNode::BinaryOp { .. }));
                }
                other => panic!("expected If, got {other:?}"),
            },
            other => panic!("expected root Block, got {other:?}"),
        }
    }

    #[test]
    fn step_tree_signature_is_stable_with_cond_ast() {
        // Phase 119: cond_ast should NOT affect signature stability.
        // Signature is based on cond_sig (AstSummary), not cond_ast.
        let ast = vec![
            ASTNode::Loop {
                condition: Box::new(ASTNode::Literal {
                    value: LiteralValue::Bool(true),
                    span: Span::unknown(),
                }),
                body: vec![assign_x(1)],
                span: Span::unknown(),
            },
        ];

        let tree1 = StepTreeBuilderBox::build_from_block(&ast);
        let tree2 = StepTreeBuilderBox::build_from_block(&ast);

        // Signature should be identical (deterministic)
        assert_eq!(tree1.signature, tree2.signature);

        // cond_ast should be populated
        match &tree1.root {
            StepNode::Block(nodes) => match &nodes[0] {
                StepNode::Loop { cond_ast, .. } => {
                    assert!(matches!(&cond_ast.0.as_ref(), ASTNode::Literal { .. }));
                }
                other => panic!("expected Loop, got {other:?}"),
            },
            other => panic!("expected root Block, got {other:?}"),
        }
    }

    #[test]
    fn contract_extracts_loop_exits_and_writes_minimal() {
        fn var(name: &str) -> ASTNode {
            ASTNode::Variable {
                name: name.to_string(),
                span: Span::unknown(),
            }
        }
        fn int_lit(v: i64) -> ASTNode {
            ASTNode::Literal {
                value: LiteralValue::Integer(v),
                span: Span::unknown(),
            }
        }
        fn bin(op: BinaryOperator, lhs: ASTNode, rhs: ASTNode) -> ASTNode {
            ASTNode::BinaryOp {
                operator: op,
                left: Box::new(lhs),
                right: Box::new(rhs),
                span: Span::unknown(),
            }
        }
        fn assign(name: &str, value: ASTNode) -> ASTNode {
            ASTNode::Assignment {
                target: Box::new(var(name)),
                value: Box::new(value),
                span: Span::unknown(),
            }
        }

        // local i=0; local x=0;
        // loop(i < 3) { x = x + 1; if x == 2 { break } i = i + 1 }
        let ast = vec![
            ASTNode::Local {
                variables: vec!["i".to_string()],
                initial_values: vec![Some(Box::new(int_lit(0)))],
                span: Span::unknown(),
            },
            ASTNode::Local {
                variables: vec!["x".to_string()],
                initial_values: vec![Some(Box::new(int_lit(0)))],
                span: Span::unknown(),
            },
            ASTNode::Loop {
                condition: Box::new(bin(BinaryOperator::Less, var("i"), int_lit(3))),
                body: vec![
                    assign("x", bin(BinaryOperator::Add, var("x"), int_lit(1))),
                    ASTNode::If {
                        condition: Box::new(bin(BinaryOperator::Equal, var("x"), int_lit(2))),
                        then_body: vec![ASTNode::Break { span: Span::unknown() }],
                        else_body: None,
                        span: Span::unknown(),
                    },
                    assign("i", bin(BinaryOperator::Add, var("i"), int_lit(1))),
                ],
                span: Span::unknown(),
            },
        ];

        let tree = StepTreeBuilderBox::build_from_block(&ast);
        assert!(tree.features.has_loop);
        assert!(tree.contract.exits.contains(&ExitKind::Break));
        assert!(tree.contract.writes.contains("i"));
        assert!(tree.contract.writes.contains("x"));
        assert!(tree.contract.required_caps.contains(&StepCapability::Loop));
        assert!(tree.contract.required_caps.contains(&StepCapability::If));
    }
}
