//! Phase 29ai P0: LoopFacts (Facts SSOT) — skeleton
//!
//! P0 contract:
//! - `Ok(Some(facts))`: facts are available for this loop
//! - `Ok(None)`: not applicable (fall back to existing routing)
//! - `Err(freeze_tag)`: contract violation (fail-fast)

#![allow(dead_code)]

use crate::ast::ASTNode;

use super::scan_shapes::{ConditionShape, StepShape};
use crate::mir::builder::control_flow::plan::planner::{Freeze, PlannerContext};
use crate::mir::loop_pattern_detection::LoopPatternKind;
use crate::ast::{BinaryOperator, LiteralValue};
use super::scan_shapes::LengthMethod;
use super::pattern1_simplewhile_facts::{
    Pattern1SimpleWhileFacts, try_extract_pattern1_simplewhile_facts,
};
use super::pattern3_ifphi_facts::{
    Pattern3IfPhiFacts, try_extract_pattern3_ifphi_facts,
};
use super::pattern4_continue_facts::{
    Pattern4ContinueFacts, try_extract_pattern4_continue_facts,
};
use super::pattern5_infinite_early_exit_facts::{
    Pattern5InfiniteEarlyExitFacts, try_extract_pattern5_infinite_early_exit_facts,
};
use super::pattern8_bool_predicate_scan_facts::{
    Pattern8BoolPredicateScanFacts, try_extract_pattern8_bool_predicate_scan_facts,
};
use super::pattern9_accum_const_loop_facts::{
    Pattern9AccumConstLoopFacts, try_extract_pattern9_accum_const_loop_facts,
};
use super::pattern2_break_facts::{Pattern2BreakFacts, try_extract_pattern2_break_facts};
use super::pattern2_loopbodylocal_facts::{
    Pattern2LoopBodyLocalFacts, try_extract_pattern2_loopbodylocal_facts,
};
use super::skeleton_facts::{SkeletonFacts, try_extract_loop_skeleton_facts};
use super::feature_facts::{LoopFeatureFacts, try_extract_loop_feature_facts};

#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct LoopFacts {
    pub condition_shape: ConditionShape,
    pub step_shape: StepShape,
    pub skeleton: SkeletonFacts,
    pub features: LoopFeatureFacts,
    pub scan_with_init: Option<ScanWithInitFacts>,
    pub split_scan: Option<SplitScanFacts>,
    pub pattern1_simplewhile: Option<Pattern1SimpleWhileFacts>,
    pub pattern3_ifphi: Option<Pattern3IfPhiFacts>,
    pub pattern4_continue: Option<Pattern4ContinueFacts>,
    pub pattern5_infinite_early_exit: Option<Pattern5InfiniteEarlyExitFacts>,
    pub pattern8_bool_predicate_scan: Option<Pattern8BoolPredicateScanFacts>,
    pub pattern9_accum_const_loop: Option<Pattern9AccumConstLoopFacts>,
    pub pattern2_break: Option<Pattern2BreakFacts>,
    pub pattern2_loopbodylocal: Option<Pattern2LoopBodyLocalFacts>,
}

#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct ScanWithInitFacts {
    pub loop_var: String,
    pub haystack: String,
    pub needle: String,
    pub step_lit: i64,
}

#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct SplitScanFacts {
    pub s_var: String,
    pub sep_var: String,
    pub result_var: String,
    pub i_var: String,
    pub start_var: String,
}

pub(in crate::mir::builder) fn try_build_loop_facts(
    condition: &ASTNode,
    body: &[ASTNode],
) -> Result<Option<LoopFacts>, Freeze> {
    try_build_loop_facts_inner(condition, body, true, true)
}

pub(in crate::mir::builder) fn try_build_loop_facts_with_ctx(
    ctx: &PlannerContext,
    condition: &ASTNode,
    body: &[ASTNode],
) -> Result<Option<LoopFacts>, Freeze> {
    let allow_pattern1 = match ctx.pattern_kind {
        Some(LoopPatternKind::Pattern1SimpleWhile) => true,
        Some(_) => false,
        None => true,
    };
    let allow_pattern8 = !ctx.in_static_box;
    try_build_loop_facts_inner(condition, body, allow_pattern1, allow_pattern8)
}

fn try_build_loop_facts_inner(
    condition: &ASTNode,
    body: &[ASTNode],
    allow_pattern1: bool,
    allow_pattern8: bool,
) -> Result<Option<LoopFacts>, Freeze> {
    // Phase 29ai P4/P7: keep Facts conservative; only return Some when we can
    // build a concrete pattern fact set (no guesses / no hardcoded names).

    let condition_shape =
        try_extract_condition_shape(condition)?.unwrap_or(ConditionShape::Unknown);
    let step_shape = try_extract_step_shape(body)?.unwrap_or(StepShape::Unknown);
    let scan_with_init = try_extract_scan_with_init_facts(body, &condition_shape, &step_shape)?;
    let split_scan = try_extract_split_scan_facts(condition, body)?;
    let pattern1_simplewhile = if allow_pattern1 {
        try_extract_pattern1_simplewhile_facts(condition, body)?
    } else {
        None
    };
    let pattern3_ifphi = try_extract_pattern3_ifphi_facts(condition, body)?;
    let pattern4_continue = try_extract_pattern4_continue_facts(condition, body)?;
    let pattern5_infinite_early_exit =
        try_extract_pattern5_infinite_early_exit_facts(condition, body)?;
    let pattern8_bool_predicate_scan = if allow_pattern8 {
        try_extract_pattern8_bool_predicate_scan_facts(
            condition,
            body,
            &condition_shape,
            &step_shape,
        )?
    } else {
        None
    };
    let pattern9_accum_const_loop = try_extract_pattern9_accum_const_loop_facts(condition, body)?;
    let pattern2_break = try_extract_pattern2_break_facts(condition, body)?;
    let pattern2_loopbodylocal = try_extract_pattern2_loopbodylocal_facts(condition, body)?;

    let has_any = scan_with_init.is_some()
        || split_scan.is_some()
        || pattern1_simplewhile.is_some()
        || pattern3_ifphi.is_some()
        || pattern4_continue.is_some()
        || pattern5_infinite_early_exit.is_some()
        || pattern8_bool_predicate_scan.is_some()
        || pattern9_accum_const_loop.is_some()
        || pattern2_break.is_some()
        || pattern2_loopbodylocal.is_some();
    if !has_any {
        return Ok(None);
    }

    let skeleton = match try_extract_loop_skeleton_facts(condition, body)? {
        Some(skeleton) => skeleton,
        None => {
            return Err(Freeze::bug(
                "loop facts require skeleton when patterns are present",
            ));
        }
    };
    let features = try_extract_loop_feature_facts(body)?;

    Ok(Some(LoopFacts {
        condition_shape,
        step_shape,
        skeleton,
        features,
        scan_with_init,
        split_scan,
        pattern1_simplewhile,
        pattern3_ifphi,
        pattern4_continue,
        pattern5_infinite_early_exit,
        pattern8_bool_predicate_scan,
        pattern9_accum_const_loop,
        pattern2_break,
        pattern2_loopbodylocal,
    }))
}

fn try_extract_condition_shape(condition: &ASTNode) -> Result<Option<ConditionShape>, Freeze> {
    let ASTNode::BinaryOp {
        operator: BinaryOperator::Less,
        left,
        right,
        ..
    } = condition
    else {
        return Ok(None);
    };

    let ASTNode::Variable { name: idx_var, .. } = left.as_ref() else {
        return Ok(None);
    };

    let ASTNode::MethodCall {
        object,
        method,
        arguments,
        ..
    } = right.as_ref()
    else {
        return Ok(None);
    };
    if !arguments.is_empty() {
        return Ok(None);
    }
    let method = match method.as_str() {
        "length" => LengthMethod::Length,
        "size" => LengthMethod::Size,
        _ => return Ok(None),
    };
    let ASTNode::Variable {
        name: haystack_var,
        ..
    } = object.as_ref()
    else {
        return Ok(None);
    };

    Ok(Some(ConditionShape::VarLessLength {
        idx_var: idx_var.clone(),
        haystack_var: haystack_var.clone(),
        method,
    }))
}

fn try_extract_step_shape(body: &[ASTNode]) -> Result<Option<StepShape>, Freeze> {
    let Some(last) = body.last() else {
        return Ok(None);
    };

    let ASTNode::Assignment { target, value, .. } = last else {
        return Ok(None);
    };
    let ASTNode::Variable { name: var, .. } = target.as_ref() else {
        return Ok(None);
    };

    let ASTNode::BinaryOp {
        operator: BinaryOperator::Add,
        left,
        right,
        ..
    } = value.as_ref()
    else {
        return Ok(None);
    };
    let ASTNode::Variable { name: lhs, .. } = left.as_ref() else {
        return Ok(None);
    };
    if lhs != var {
        return Ok(None);
    }

    let ASTNode::Literal { value, .. } = right.as_ref() else {
        return Ok(None);
    };
    let LiteralValue::Integer(k) = value else {
        return Ok(None);
    };

    if *k != 1 {
        return Ok(None);
    }

    Ok(Some(StepShape::AssignAddConst {
        var: var.clone(),
        k: *k,
    }))
}

#[cfg(test)]
mod tests {
    use super::{try_build_loop_facts, LoopFacts};
    use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
    use crate::mir::builder::control_flow::plan::facts::skeleton_facts::SkeletonKind;

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

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

    #[test]
    fn loop_facts_require_skeleton_and_features_when_present() {
        let condition = ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(v("i")),
            right: Box::new(lit_int(3)),
            span: Span::unknown(),
        };
        let body = vec![ASTNode::Assignment {
            target: Box::new(v("i")),
            value: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Add,
                left: Box::new(v("i")),
                right: Box::new(lit_int(1)),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        }];

        let facts = try_build_loop_facts(&condition, &body)
            .expect("Ok")
            .expect("Some");
        assert_eq!(facts.skeleton.kind, SkeletonKind::Loop);
        assert!(!facts.features.exit_usage.has_break);
        assert!(!facts.features.exit_usage.has_continue);
        assert!(!facts.features.exit_usage.has_return);
        let _: LoopFacts = facts;
    }
}

fn try_extract_scan_with_init_facts(
    body: &[ASTNode],
    condition_shape: &ConditionShape,
    step_shape: &StepShape,
) -> Result<Option<ScanWithInitFacts>, Freeze> {
    let (ConditionShape::VarLessLength {
        idx_var,
        haystack_var,
        ..
    }, StepShape::AssignAddConst { var: step_var, k: 1 }) = (condition_shape, step_shape)
    else {
        return Ok(None);
    };

    if step_var != idx_var {
        return Ok(None);
    }

    // Find `if s.substring(i, i + 1) == ch { return i }` anywhere except the last step.
    for stmt in body.iter().take(body.len().saturating_sub(1)) {
        let ASTNode::If {
            condition,
            then_body,
            else_body,
            ..
        } = stmt
        else {
            continue;
        };
        if else_body.is_some() {
            continue;
        }

        let ASTNode::BinaryOp {
            operator: BinaryOperator::Equal,
            left,
            right,
            ..
        } = condition.as_ref()
        else {
            continue;
        };

        let ASTNode::MethodCall {
            object,
            method,
            arguments,
            ..
        } = left.as_ref()
        else {
            continue;
        };
        if method != "substring" || arguments.len() != 2 {
            continue;
        }

        let ASTNode::Variable { name: obj, .. } = object.as_ref() else {
            continue;
        };
        if obj != haystack_var {
            continue;
        }

        // substring(i, i + 1)
        let (start, end) = (&arguments[0], &arguments[1]);
        match start {
            ASTNode::Variable { name, .. } if name == idx_var => {}
            _ => continue,
        }
        let ASTNode::BinaryOp {
            operator: BinaryOperator::Add,
            left: end_left,
            right: end_right,
            ..
        } = end
        else {
            continue;
        };
        match end_left.as_ref() {
            ASTNode::Variable { name, .. } if name == idx_var => {}
            _ => continue,
        }
        match end_right.as_ref() {
            ASTNode::Literal {
                value: LiteralValue::Integer(1),
                ..
            } => {}
            _ => continue,
        }

        let ASTNode::Variable { name: needle, .. } = right.as_ref() else {
            continue;
        };

        // then-body must contain `return i` (minimal)
        if !then_body.iter().any(|n| {
            matches!(
                n,
                ASTNode::Return {
                    value: Some(v),
                    ..
                } if matches!(v.as_ref(), ASTNode::Variable { name, .. } if name == idx_var)
            )
        }) {
            continue;
        }

        return Ok(Some(ScanWithInitFacts {
            loop_var: idx_var.clone(),
            haystack: haystack_var.clone(),
            needle: needle.clone(),
            step_lit: 1,
        }));
    }

    Ok(None)
}

fn try_extract_split_scan_facts(
    condition: &ASTNode,
    body: &[ASTNode],
) -> Result<Option<SplitScanFacts>, Freeze> {
    let Some((i_var, s_var, sep_var)) = try_extract_split_scan_condition_vars(condition) else {
        return Ok(None);
    };

    let Some(if_stmt) = body.iter().find_map(|stmt| match stmt {
        ASTNode::If {
            condition,
            then_body,
            else_body,
            ..
        } => Some((condition.as_ref(), then_body, else_body.as_ref())),
        _ => None,
    }) else {
        return Ok(None);
    };
    let (match_condition, then_body, else_body) = if_stmt;
    let Some(else_body) = else_body else {
        return Ok(None);
    };

    if !is_split_scan_match_condition(match_condition, &s_var, &i_var, &sep_var) {
        return Ok(None);
    }

    let Some((result_var, start_var)) =
        then_body
            .iter()
            .find_map(|stmt| extract_split_scan_then_push(stmt, &s_var, &i_var))
    else {
        return Ok(None);
    };

    let has_start_update = then_body
        .iter()
        .any(|stmt| is_start_update(stmt, &start_var, &i_var, &sep_var));
    if !has_start_update {
        return Ok(None);
    }

    let has_i_set_to_start = then_body
        .iter()
        .any(|stmt| is_i_set_to_start(stmt, &i_var, &start_var));
    if !has_i_set_to_start {
        return Ok(None);
    }

    let has_else_increment = else_body
        .iter()
        .any(|stmt| is_i_increment_by_one(stmt, &i_var));
    if !has_else_increment {
        return Ok(None);
    }

    Ok(Some(SplitScanFacts {
        s_var,
        sep_var,
        result_var,
        i_var,
        start_var,
    }))
}

fn try_extract_split_scan_condition_vars(condition: &ASTNode) -> Option<(String, String, String)> {
    let ASTNode::BinaryOp {
        operator: BinaryOperator::LessEqual,
        left,
        right,
        ..
    } = condition
    else {
        return None;
    };
    let ASTNode::Variable { name: i_var, .. } = left.as_ref() else {
        return None;
    };
    let ASTNode::BinaryOp {
        operator: BinaryOperator::Subtract,
        left: minus_left,
        right: minus_right,
        ..
    } = right.as_ref()
    else {
        return None;
    };
    let s_var = match minus_left.as_ref() {
        ASTNode::MethodCall {
            object,
            method,
            arguments,
            ..
        } if method == "length" && arguments.is_empty() => match object.as_ref() {
            ASTNode::Variable { name, .. } => name.clone(),
            _ => return None,
        },
        _ => return None,
    };
    let sep_var = match minus_right.as_ref() {
        ASTNode::MethodCall {
            object,
            method,
            arguments,
            ..
        } if method == "length" && arguments.is_empty() => match object.as_ref() {
            ASTNode::Variable { name, .. } => name.clone(),
            _ => return None,
        },
        _ => return None,
    };

    Some((i_var.clone(), s_var, sep_var))
}

fn is_split_scan_match_condition(
    condition: &ASTNode,
    s_var: &str,
    i_var: &str,
    sep_var: &str,
) -> bool {
    let ASTNode::BinaryOp {
        operator: BinaryOperator::Equal,
        left,
        right,
        ..
    } = condition
    else {
        return false;
    };

    let matches_left = is_substring_i_plus_sep_len(left.as_ref(), s_var, i_var, sep_var)
        && matches!(
            right.as_ref(),
            ASTNode::Variable { name, .. } if name == sep_var
        );
    let matches_right = is_substring_i_plus_sep_len(right.as_ref(), s_var, i_var, sep_var)
        && matches!(
            left.as_ref(),
            ASTNode::Variable { name, .. } if name == sep_var
        );
    matches_left || matches_right
}

fn is_substring_i_plus_sep_len(
    expr: &ASTNode,
    s_var: &str,
    i_var: &str,
    sep_var: &str,
) -> bool {
    let ASTNode::MethodCall {
        object,
        method,
        arguments,
        ..
    } = expr
    else {
        return false;
    };
    if method != "substring" || arguments.len() != 2 {
        return false;
    }
    let ASTNode::Variable { name: obj, .. } = object.as_ref() else {
        return false;
    };
    if obj != s_var {
        return false;
    }

    match &arguments[0] {
        ASTNode::Variable { name, .. } if name == i_var => {}
        _ => return false,
    }

    let ASTNode::BinaryOp {
        operator: BinaryOperator::Add,
        left,
        right,
        ..
    } = &arguments[1]
    else {
        return false;
    };
    match left.as_ref() {
        ASTNode::Variable { name, .. } if name == i_var => {}
        _ => return false,
    }
    matches!(
        right.as_ref(),
        ASTNode::MethodCall { object, method, arguments, .. }
            if method == "length"
                && arguments.is_empty()
                && matches!(object.as_ref(), ASTNode::Variable { name, .. } if name == sep_var)
    )
}

fn extract_split_scan_then_push(
    stmt: &ASTNode,
    s_var: &str,
    i_var: &str,
) -> Option<(String, String)> {
    let ASTNode::MethodCall {
        object,
        method,
        arguments,
        ..
    } = stmt
    else {
        return None;
    };
    if method != "push" || arguments.len() != 1 {
        return None;
    }
    let ASTNode::Variable { name: result_var, .. } = object.as_ref() else {
        return None;
    };
    let ASTNode::MethodCall {
        object: substr_object,
        method: substr_method,
        arguments: substr_args,
        ..
    } = &arguments[0]
    else {
        return None;
    };
    if substr_method != "substring" || substr_args.len() != 2 {
        return None;
    }
    let ASTNode::Variable { name: substr_obj, .. } = substr_object.as_ref() else {
        return None;
    };
    if substr_obj != s_var {
        return None;
    }
    let ASTNode::Variable { name: start_var, .. } = &substr_args[0] else {
        return None;
    };
    match &substr_args[1] {
        ASTNode::Variable { name, .. } if name == i_var => {}
        _ => return None,
    }

    Some((result_var.clone(), start_var.clone()))
}

fn is_start_update(stmt: &ASTNode, start_var: &str, i_var: &str, sep_var: &str) -> bool {
    let ASTNode::Assignment { target, value, .. } = stmt else {
        return false;
    };
    match target.as_ref() {
        ASTNode::Variable { name, .. } if name == start_var => {}
        _ => return false,
    }
    let ASTNode::BinaryOp {
        operator: BinaryOperator::Add,
        left,
        right,
        ..
    } = value.as_ref()
    else {
        return false;
    };
    match left.as_ref() {
        ASTNode::Variable { name, .. } if name == i_var => {}
        _ => return false,
    }
    matches!(
        right.as_ref(),
        ASTNode::MethodCall { object, method, arguments, .. }
            if method == "length"
                && arguments.is_empty()
                && matches!(object.as_ref(), ASTNode::Variable { name, .. } if name == sep_var)
    )
}

fn is_i_set_to_start(stmt: &ASTNode, i_var: &str, start_var: &str) -> bool {
    let ASTNode::Assignment { target, value, .. } = stmt else {
        return false;
    };
    matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var)
        && matches!(value.as_ref(), ASTNode::Variable { name, .. } if name == start_var)
}

fn is_i_increment_by_one(stmt: &ASTNode, i_var: &str) -> bool {
    let ASTNode::Assignment { target, value, .. } = stmt else {
        return false;
    };
    let target_ok = matches!(target.as_ref(), ASTNode::Variable { name, .. } if name == i_var);
    let value_ok = matches!(
        value.as_ref(),
        ASTNode::BinaryOp {
            operator: BinaryOperator::Add,
            left,
            right,
            ..
        } if matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == i_var)
            && matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(1), .. })
    );
    target_ok && value_ok
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ast::Span;
    use crate::mir::builder::control_flow::plan::planner::PlannerContext;
    use crate::mir::loop_pattern_detection::LoopPatternKind;

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

    #[test]
    fn loopfacts_ok_some_for_canonical_scan_with_init_minimal() {
        let condition = ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(v("i")),
            right: Box::new(ASTNode::MethodCall {
                object: Box::new(v("s")),
                method: "length".to_string(),
                arguments: vec![],
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };
        let if_stmt = ASTNode::If {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Equal,
                left: Box::new(ASTNode::MethodCall {
                    object: Box::new(v("s")),
                    method: "substring".to_string(),
                    arguments: vec![
                        v("i"),
                        ASTNode::BinaryOp {
                            operator: BinaryOperator::Add,
                            left: Box::new(v("i")),
                            right: Box::new(ASTNode::Literal {
                                value: LiteralValue::Integer(1),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        },
                    ],
                    span: Span::unknown(),
                }),
                right: Box::new(v("ch")),
                span: Span::unknown(),
            }),
            then_body: vec![ASTNode::Return {
                value: Some(Box::new(v("i"))),
                span: Span::unknown(),
            }],
            else_body: None,
            span: Span::unknown(),
        };
        let step = ASTNode::Assignment {
            target: Box::new(v("i")),
            value: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Add,
                left: Box::new(v("i")),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(1),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };

        let facts = try_build_loop_facts(&condition, &[if_stmt, step]).expect("Ok");
        assert!(facts.is_some());
    }

    #[test]
    fn loopfacts_ctx_skips_pattern1_when_kind_mismatch() {
        let condition = ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(v("i")),
            right: Box::new(ASTNode::Literal {
                value: LiteralValue::Integer(3),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };
        let step = ASTNode::Assignment {
            target: Box::new(v("i")),
            value: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Add,
                left: Box::new(v("i")),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(1),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };
        let ctx = PlannerContext {
            pattern_kind: Some(LoopPatternKind::Pattern2Break),
            in_static_box: false,
            debug: false,
        };

        let facts = try_build_loop_facts_with_ctx(&ctx, &condition, &[step]).expect("Ok");
        assert!(facts.is_none());
    }

    #[test]
    fn loopfacts_ctx_allows_pattern1_when_kind_matches() {
        let condition = ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(v("i")),
            right: Box::new(ASTNode::Literal {
                value: LiteralValue::Integer(3),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };
        let step = ASTNode::Assignment {
            target: Box::new(v("i")),
            value: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Add,
                left: Box::new(v("i")),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(1),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };
        let ctx = PlannerContext {
            pattern_kind: Some(LoopPatternKind::Pattern1SimpleWhile),
            in_static_box: false,
            debug: false,
        };

        let facts = try_build_loop_facts_with_ctx(&ctx, &condition, &[step]).expect("Ok");
        let facts = facts.expect("Some");
        assert!(facts.pattern1_simplewhile.is_some());
    }

    #[test]
    fn loopfacts_ctx_blocks_pattern8_in_static_box() {
        let condition = ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(v("i")),
            right: Box::new(ASTNode::MethodCall {
                object: Box::new(v("s")),
                method: "length".to_string(),
                arguments: vec![],
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };
        let predicate_if = ASTNode::If {
            condition: Box::new(ASTNode::UnaryOp {
                operator: crate::ast::UnaryOperator::Not,
                operand: Box::new(ASTNode::MethodCall {
                    object: Box::new(ASTNode::Me {
                        span: Span::unknown(),
                    }),
                    method: "is_digit".to_string(),
                    arguments: vec![ASTNode::MethodCall {
                        object: Box::new(v("s")),
                        method: "substring".to_string(),
                        arguments: vec![
                            v("i"),
                            ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(v("i")),
                                right: Box::new(ASTNode::Literal {
                                    value: LiteralValue::Integer(1),
                                    span: Span::unknown(),
                                }),
                                span: Span::unknown(),
                            },
                        ],
                        span: Span::unknown(),
                    }],
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            then_body: vec![ASTNode::Return {
                value: Some(Box::new(ASTNode::Literal {
                    value: LiteralValue::Bool(false),
                    span: Span::unknown(),
                })),
                span: Span::unknown(),
            }],
            else_body: None,
            span: Span::unknown(),
        };
        let step = ASTNode::Assignment {
            target: Box::new(v("i")),
            value: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Add,
                left: Box::new(v("i")),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(1),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };
        let body = vec![predicate_if, step];
        let allow_ctx = PlannerContext {
            pattern_kind: None,
            in_static_box: false,
            debug: false,
        };
        let block_ctx = PlannerContext {
            pattern_kind: None,
            in_static_box: true,
            debug: false,
        };

        let allow = try_build_loop_facts_with_ctx(&allow_ctx, &condition, &body).expect("Ok");
        assert!(allow
            .as_ref()
            .and_then(|facts| facts.pattern8_bool_predicate_scan.as_ref())
            .is_some());

        let blocked = try_build_loop_facts_with_ctx(&block_ctx, &condition, &body).expect("Ok");
        assert!(blocked
            .as_ref()
            .and_then(|facts| facts.pattern8_bool_predicate_scan.as_ref())
            .is_none());
    }

    #[test]
    fn loopfacts_ok_none_when_condition_not_supported() {
        let condition = v("i"); // not `i < n`
        let facts = try_build_loop_facts(&condition, &[]).expect("Ok");
        assert!(facts.is_none());
    }

    #[test]
    fn loopfacts_ok_none_when_step_var_differs_from_condition_var() {
        let condition = ASTNode::BinaryOp {
            operator: BinaryOperator::Less,
            left: Box::new(v("i")),
            right: Box::new(ASTNode::MethodCall {
                object: Box::new(v("s")),
                method: "length".to_string(),
                arguments: vec![],
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };
        let step = ASTNode::Assignment {
            target: Box::new(v("j")),
            value: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Add,
                left: Box::new(v("j")),
                right: Box::new(ASTNode::Literal {
                    value: LiteralValue::Integer(1),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };

        let facts = try_build_loop_facts(&condition, &[step]).expect("Ok");
        assert!(facts.is_none());
    }

    #[test]
    fn loopfacts_ok_some_for_canonical_split_scan_minimal() {
        let condition = ASTNode::BinaryOp {
            operator: BinaryOperator::LessEqual,
            left: Box::new(v("i")),
            right: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Subtract,
                left: Box::new(ASTNode::MethodCall {
                    object: Box::new(v("s")),
                    method: "length".to_string(),
                    arguments: vec![],
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::MethodCall {
                    object: Box::new(v("separator")),
                    method: "length".to_string(),
                    arguments: vec![],
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };

        let if_stmt = ASTNode::If {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Equal,
                left: Box::new(ASTNode::MethodCall {
                    object: Box::new(v("s")),
                    method: "substring".to_string(),
                    arguments: vec![
                        v("i"),
                        ASTNode::BinaryOp {
                            operator: BinaryOperator::Add,
                            left: Box::new(v("i")),
                            right: Box::new(ASTNode::MethodCall {
                                object: Box::new(v("separator")),
                                method: "length".to_string(),
                                arguments: vec![],
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        },
                    ],
                    span: Span::unknown(),
                }),
                right: Box::new(v("separator")),
                span: Span::unknown(),
            }),
            then_body: vec![
                ASTNode::MethodCall {
                    object: Box::new(v("result")),
                    method: "push".to_string(),
                    arguments: vec![ASTNode::MethodCall {
                        object: Box::new(v("s")),
                        method: "substring".to_string(),
                        arguments: vec![v("start"), v("i")],
                        span: Span::unknown(),
                    }],
                    span: Span::unknown(),
                },
                ASTNode::Assignment {
                    target: Box::new(v("start")),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(v("i")),
                        right: Box::new(ASTNode::MethodCall {
                            object: Box::new(v("separator")),
                            method: "length".to_string(),
                            arguments: vec![],
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
                ASTNode::Assignment {
                    target: Box::new(v("i")),
                    value: Box::new(v("start")),
                    span: Span::unknown(),
                },
            ],
            else_body: Some(vec![ASTNode::Assignment {
                target: Box::new(v("i")),
                value: Box::new(ASTNode::BinaryOp {
                    operator: BinaryOperator::Add,
                    left: Box::new(v("i")),
                    right: Box::new(ASTNode::Literal {
                        value: LiteralValue::Integer(1),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }]),
            span: Span::unknown(),
        };

        let facts = try_build_loop_facts(&condition, &[if_stmt]).expect("Ok");
        let split_scan = facts.and_then(|facts| facts.split_scan);
        let split_scan = split_scan.expect("SplitScan facts");
        assert_eq!(split_scan.s_var, "s");
        assert_eq!(split_scan.sep_var, "separator");
        assert_eq!(split_scan.result_var, "result");
        assert_eq!(split_scan.i_var, "i");
        assert_eq!(split_scan.start_var, "start");
    }

    #[test]
    fn loopfacts_ok_none_when_split_scan_missing_else() {
        let condition = ASTNode::BinaryOp {
            operator: BinaryOperator::LessEqual,
            left: Box::new(v("i")),
            right: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Subtract,
                left: Box::new(ASTNode::MethodCall {
                    object: Box::new(v("s")),
                    method: "length".to_string(),
                    arguments: vec![],
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::MethodCall {
                    object: Box::new(v("separator")),
                    method: "length".to_string(),
                    arguments: vec![],
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            span: Span::unknown(),
        };

        let if_stmt = ASTNode::If {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Equal,
                left: Box::new(ASTNode::MethodCall {
                    object: Box::new(v("s")),
                    method: "substring".to_string(),
                    arguments: vec![
                        v("i"),
                        ASTNode::BinaryOp {
                            operator: BinaryOperator::Add,
                            left: Box::new(v("i")),
                            right: Box::new(ASTNode::MethodCall {
                                object: Box::new(v("separator")),
                                method: "length".to_string(),
                                arguments: vec![],
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        },
                    ],
                    span: Span::unknown(),
                }),
                right: Box::new(v("separator")),
                span: Span::unknown(),
            }),
            then_body: vec![ASTNode::MethodCall {
                object: Box::new(v("result")),
                method: "push".to_string(),
                arguments: vec![ASTNode::MethodCall {
                    object: Box::new(v("s")),
                    method: "substring".to_string(),
                    arguments: vec![v("start"), v("i")],
                    span: Span::unknown(),
                }],
                span: Span::unknown(),
            }],
            else_body: None,
            span: Span::unknown(),
        };

        let facts = try_build_loop_facts(&condition, &[if_stmt]).expect("Ok");
        assert!(facts.is_none());
    }
}
