//! Loop Canonicalization Entry Point
//!
//! This module provides the main canonicalization logic that converts
//! AST loops into normalized LoopSkeleton structures.

use crate::ast::ASTNode;
use crate::mir::loop_pattern_detection::LoopPatternKind;

use super::capability_guard::{CapabilityTag, RoutingDecision};
use super::pattern_recognizer::{
    try_extract_continue_pattern, try_extract_escape_skip_pattern, try_extract_parse_number_pattern,
    try_extract_parse_string_pattern, try_extract_read_digits_loop_true_pattern,
    try_extract_skip_whitespace_pattern,
};
use super::skeleton_types::{
    CarrierRole, CarrierSlot, ExitContract, LoopSkeleton, SkeletonStep, UpdateKind,
};

// ============================================================================
// Canonicalization Entry Point
// ============================================================================

/// Canonicalize a loop AST into LoopSkeleton
///
/// Phase 143-P2: Now supports parse_array pattern in addition to parse_string, skip_whitespace, parse_number, and continue
///
/// Supported patterns:
/// 1. Skip whitespace (break in ELSE clause):
/// ```
/// loop(cond) {
///     // ... optional body statements
///     if check_cond {
///         carrier = carrier + step
///     } else {
///         break
///     }
/// }
/// ```
///
/// 2. Parse number (break in THEN clause):
/// ```
/// loop(cond) {
///     // ... optional body statements (ch, digit_pos computation)
///     if invalid_cond {
///         break
///     }
///     // ... rest statements (result append)
///     carrier = carrier + step
/// }
/// ```
///
/// 3. Continue pattern:
/// ```
/// loop(cond) {
///     // ... optional body statements
///     if skip_cond {
///         carrier = carrier + step  // Optional
///         continue
///     }
///     // ... rest statements
///     carrier = carrier + step
/// }
/// ```
///
/// 4. Parse string/array (both continue AND return):
/// ```
/// loop(cond) {
///     // ... body statements
///     if stop_cond {            // quote for string, ']' for array
///         return result
///     }
///     if separator_cond {       // escape for string, ',' for array
///         // ... separator handling
///         carrier = carrier + step
///         continue
///     }
///     carrier = carrier + step
/// }
/// ```
///
/// Note: parse_string and parse_array share the same structural pattern
/// (continue + return exits) and are recognized by the same detector.
///
/// All other patterns return Fail-Fast with detailed reasoning.
///
/// # Arguments
/// - `loop_expr`: The loop AST node (must be `ASTNode::Loop`)
///
/// # Returns
/// - `Ok((skeleton, decision))`: Successfully extracted skeleton and routing decision
/// - `Err(String)`: Malformed AST or internal error
pub fn canonicalize_loop_expr(
    loop_expr: &ASTNode,
) -> Result<(LoopSkeleton, RoutingDecision), String> {
    // Extract loop components
    let (condition, body, span) = match loop_expr {
        ASTNode::Loop {
            condition,
            body,
            span,
        } => (condition.as_ref(), body, span.clone()),
        _ => return Err(format!("Expected Loop node, got: {:?}", loop_expr)),
    };

    // Phase 143-P1/P2: Try to extract parse_string/parse_array pattern first (most specific)
    // Note: Both parse_string and parse_array share the same structure (continue + return)
    if let Some((carrier_name, delta, body_stmts)) = try_extract_parse_string_pattern(body) {
        // Build skeleton for parse_string/parse_array pattern
        let mut skeleton = LoopSkeleton::new(span);

        // Step 1: Header condition
        skeleton.steps.push(SkeletonStep::HeaderCond {
            expr: Box::new(condition.clone()),
        });

        // Step 2: Body statements (if any)
        if !body_stmts.is_empty() {
            skeleton
                .steps
                .push(SkeletonStep::Body { stmts: body_stmts });
        }

        // Step 3: Update step
        skeleton.steps.push(SkeletonStep::Update {
            carrier_name: carrier_name.clone(),
            update_kind: UpdateKind::ConstStep { delta },
        });

        // Add carrier slot
        skeleton.carriers.push(CarrierSlot {
            name: carrier_name,
            role: CarrierRole::Counter,
            update_kind: UpdateKind::ConstStep { delta },
        });

        // Set exit contract for parse_string/parse_array pattern
        skeleton.exits = ExitContract {
            has_break: false,
            has_continue: true,
            has_return: true,
            break_has_value: false,
        };

        // Phase 143-P1: Route to Pattern4Continue (has both continue and return)
        let decision = RoutingDecision::success(LoopPatternKind::Pattern4Continue);
        return Ok((skeleton, decision));
    }

    // Phase 142-P1: Try to extract continue pattern
    if let Some((carrier_name, delta, body_stmts, rest_stmts)) = try_extract_continue_pattern(body)
    {
        // Build skeleton for continue pattern
        let mut skeleton = LoopSkeleton::new(span);

        // Step 1: Header condition
        skeleton.steps.push(SkeletonStep::HeaderCond {
            expr: Box::new(condition.clone()),
        });

        // Step 2: Body statements (if any)
        if !body_stmts.is_empty() {
            skeleton
                .steps
                .push(SkeletonStep::Body { stmts: body_stmts });
        }

        // Step 3: Rest statements (if any, excluding carrier update)
        // For now, we include all rest_stmts in Body
        // The actual carrier update is implicit in the Update step
        if !rest_stmts.is_empty() {
            // Remove the last statement (carrier update) from rest_stmts
            let mut rest_body = rest_stmts;
            if !rest_body.is_empty() {
                rest_body.pop();
            }
            if !rest_body.is_empty() {
                skeleton.steps.push(SkeletonStep::Body { stmts: rest_body });
            }
        }

        // Step 4: Update step
        skeleton.steps.push(SkeletonStep::Update {
            carrier_name: carrier_name.clone(),
            update_kind: UpdateKind::ConstStep { delta },
        });

        // Add carrier slot
        skeleton.carriers.push(CarrierSlot {
            name: carrier_name,
            role: CarrierRole::Counter,
            update_kind: UpdateKind::ConstStep { delta },
        });

        // Set exit contract for continue pattern
        skeleton.exits = ExitContract {
            has_break: false,
            has_continue: true,
            has_return: false,
            break_has_value: false,
        };

        // Phase 142-P1: Route to Pattern4Continue
        let decision = RoutingDecision::success(LoopPatternKind::Pattern4Continue);
        return Ok((skeleton, decision));
    }

    // Phase 104: loop(true) + break-only digits (read_digits_from)
    //
    // Shape (JsonCursorBox.read_digits_from / MiniJsonLoader.read_digits_from):
    // - loop(true)
    // - last statement is `if is_digit { ... i = i + 1 } else { break }`
    // - may have `if ch == "" { break }` and substring read before it
    if matches!(
        condition,
        ASTNode::Literal {
            value: crate::ast::LiteralValue::Bool(true),
            ..
        }
    ) {
        if let Some((carrier_name, delta, body_stmts)) = try_extract_read_digits_loop_true_pattern(body) {
            let mut skeleton = LoopSkeleton::new(span);

            skeleton.steps.push(SkeletonStep::HeaderCond {
                expr: Box::new(condition.clone()),
            });

            if !body_stmts.is_empty() {
                skeleton.steps.push(SkeletonStep::Body { stmts: body_stmts });
            }

            skeleton.steps.push(SkeletonStep::Update {
                carrier_name: carrier_name.clone(),
                update_kind: UpdateKind::ConstStep { delta },
            });

            skeleton.carriers.push(CarrierSlot {
                name: carrier_name,
                role: CarrierRole::Counter,
                update_kind: UpdateKind::ConstStep { delta },
            });

            skeleton.exits = ExitContract {
                has_break: true,
                has_continue: false,
                has_return: false,
                break_has_value: false,
            };

            let decision = RoutingDecision::success(LoopPatternKind::Pattern2Break);
            return Ok((skeleton, decision));
        }
    }

    // Phase 143-P0: Try to extract parse_number pattern (break in THEN clause)
    if let Some((carrier_name, delta, body_stmts, rest_stmts)) =
        try_extract_parse_number_pattern(body)
    {
        // Build skeleton for parse_number pattern
        let mut skeleton = LoopSkeleton::new(span);

        // Step 1: Header condition
        skeleton.steps.push(SkeletonStep::HeaderCond {
            expr: Box::new(condition.clone()),
        });

        // Step 2: Body statements before break check (if any)
        if !body_stmts.is_empty() {
            skeleton
                .steps
                .push(SkeletonStep::Body { stmts: body_stmts });
        }

        // Step 3: Rest statements after break check (if any, excluding carrier update)
        // The carrier update is implicit in the Update step
        if !rest_stmts.is_empty() {
            // Remove the last statement (carrier update) from rest_stmts
            let mut rest_body = rest_stmts;
            if !rest_body.is_empty() {
                rest_body.pop();
            }
            if !rest_body.is_empty() {
                skeleton.steps.push(SkeletonStep::Body { stmts: rest_body });
            }
        }

        // Step 4: Update step
        skeleton.steps.push(SkeletonStep::Update {
            carrier_name: carrier_name.clone(),
            update_kind: UpdateKind::ConstStep { delta },
        });

        // Add carrier slot
        skeleton.carriers.push(CarrierSlot {
            name: carrier_name,
            role: CarrierRole::Counter,
            update_kind: UpdateKind::ConstStep { delta },
        });

        // Set exit contract for parse_number pattern
        skeleton.exits = ExitContract {
            has_break: true,
            has_continue: false,
            has_return: false,
            break_has_value: false,
        };

        // Phase 143-P0: Route to Pattern2Break (has_break=true)
        let decision = RoutingDecision::success(LoopPatternKind::Pattern2Break);
        return Ok((skeleton, decision));
    }

    // Phase 3: Try to extract skip_whitespace pattern
    if let Some((carrier_name, delta, body_stmts)) = try_extract_skip_whitespace_pattern(body) {
        // Build skeleton for skip_whitespace pattern
        let mut skeleton = LoopSkeleton::new(span);

        // Step 1: Header condition
        skeleton.steps.push(SkeletonStep::HeaderCond {
            expr: Box::new(condition.clone()),
        });

        // Step 2: Body statements (if any)
        if !body_stmts.is_empty() {
            skeleton
                .steps
                .push(SkeletonStep::Body { stmts: body_stmts });
        }

        // Step 3: Update step
        skeleton.steps.push(SkeletonStep::Update {
            carrier_name: carrier_name.clone(),
            update_kind: UpdateKind::ConstStep { delta },
        });

        // Add carrier slot
        skeleton.carriers.push(CarrierSlot {
            name: carrier_name,
            role: CarrierRole::Counter,
            update_kind: UpdateKind::ConstStep { delta },
        });

        // Set exit contract
        skeleton.exits = ExitContract {
            has_break: true,
            has_continue: false,
            has_return: false,
            break_has_value: false,
        };

        // Phase 137-5: Decision policy SSOT - ExitContract determines pattern choice
        // Since has_break=true, this should route to Pattern2Break (not Pattern3IfPhi)
        // Pattern3IfPhi is for if-else PHI *without* break statements
        let decision = RoutingDecision::success(LoopPatternKind::Pattern2Break);
        return Ok((skeleton, decision));
    }

    // ========================================================================
    // Phase 91 P5b: Escape Sequence Handling Pattern
    // ========================================================================
    // Position: After skip_whitespace (post-existing patterns)
    // Purpose: Recognize escape sequence handling in string parsers
    // Chosen: Pattern2Break (same as skip_whitespace, but with richer Skeleton)
    // Notes: Added for parity/observability, lowering deferred to Phase 92

    // Phase 92 P0-3: Now also extracts escape_cond for JoinIR Select generation
    if let Some((counter_name, normal_delta, escape_delta, _quote_char, _escape_char, body_stmts, escape_cond)) =
        try_extract_escape_skip_pattern(body)
    {
        // Build skeleton for escape skip pattern (P5b)
        let mut skeleton = LoopSkeleton::new(span);

        // Step 1: Header condition
        skeleton.steps.push(SkeletonStep::HeaderCond {
            expr: Box::new(condition.clone()),
        });

        // Step 2: Body statements (if any)
        if !body_stmts.is_empty() {
            skeleton
                .steps
                .push(SkeletonStep::Body { stmts: body_stmts });
        }

        // Step 3: Update step with ConditionalStep (escape_delta vs normal_delta)
        // Pattern: normal i = i + 1, escape i = i + escape_delta (e.g., +2)
        // Represented as UpdateKind::ConditionalStep with both deltas and condition
        // Phase 92 P0-3: Now includes escape_cond for JoinIR Select generation
        skeleton.steps.push(SkeletonStep::Update {
            carrier_name: counter_name.clone(),
            update_kind: UpdateKind::ConditionalStep {
                cond: escape_cond.clone(),  // Phase 92 P0-3: Condition for Select
                then_delta: escape_delta,    // Escape branch: +2 or other
                else_delta: normal_delta,    // Normal branch: +1
            },
        });

        // Add carrier slot with conditional step update
        skeleton.carriers.push(CarrierSlot {
            name: counter_name,
            role: CarrierRole::Counter,
            update_kind: UpdateKind::ConditionalStep {
                cond: escape_cond,           // Phase 92 P0-3: Condition for Select
                then_delta: escape_delta,
                else_delta: normal_delta,
            },
        });

        // Set exit contract (P5b has break for string boundary detection)
        skeleton.exits = ExitContract {
            has_break: true,
            has_continue: false,
            has_return: false,
            break_has_value: false,
        };

        // Phase 91 P5b Decision Policy:
        // Same as skip_whitespace (Pattern2Break)
        // P5b is a "detailed version" of Pattern2, not a separate chosen pattern
        // Notes field would record escape-specific details (Phase 91 MVP: omitted)
        let decision = RoutingDecision::success(LoopPatternKind::Pattern2Break);
        return Ok((skeleton, decision));
    }

    // Pattern not recognized - fail fast
    Ok((
        LoopSkeleton::new(span),
        RoutingDecision::fail_fast(
            vec![CapabilityTag::ConstStep],
            "Phase 143-P2: Loop does not match read_digits(loop(true)), skip_whitespace, parse_number, continue, parse_string, or parse_array pattern"
                .to_string(),
        ),
    ))
}

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

    #[test]
    fn test_canonicalize_rejects_non_loop() {
        let not_loop = ASTNode::Literal {
            value: LiteralValue::Bool(true),
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&not_loop);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Expected Loop node"));
    }

    #[test]
    fn test_skip_whitespace_pattern_recognition() {
        // Build skip_whitespace pattern: loop(p < len) { if is_ws == 1 { p = p + 1 } else { break } }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "p".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "len".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![ASTNode::If {
                condition: Box::new(ASTNode::BinaryOp {
                    operator: BinaryOperator::Equal,
                    left: Box::new(ASTNode::Variable {
                        name: "is_ws".to_string(),
                        span: Span::unknown(),
                    }),
                    right: Box::new(ASTNode::Literal {
                        value: LiteralValue::Integer(1),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }),
                then_body: vec![ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "p".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "p".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(1),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }],
                else_body: Some(vec![ASTNode::Break {
                    span: Span::unknown(),
                }]),
                span: Span::unknown(),
            }],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (skeleton, decision) = result.unwrap();

        // Verify success
        assert!(decision.is_success());
        // Phase 137-5: Pattern choice reflects ExitContract (has_break=true → Pattern2Break)
        assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break));
        assert_eq!(decision.missing_caps.len(), 0);

        // Verify skeleton structure
        assert_eq!(skeleton.steps.len(), 2); // HeaderCond + Update
        assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));
        assert!(matches!(skeleton.steps[1], SkeletonStep::Update { .. }));

        // Verify carrier
        assert_eq!(skeleton.carriers.len(), 1);
        assert_eq!(skeleton.carriers[0].name, "p");
        assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
        match &skeleton.carriers[0].update_kind {
            UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
            _ => panic!("Expected ConstStep update"),
        }

        // Verify exit contract
        assert!(skeleton.exits.has_break);
        assert!(!skeleton.exits.has_continue);
        assert!(!skeleton.exits.has_return);
        assert!(!skeleton.exits.break_has_value);
    }

    #[test]
    fn test_skip_whitespace_with_body_statements() {
        // Build pattern with body statements before the if:
        // loop(p < len) {
        //   local ch = get_char(p)
        //   if is_ws { p = p + 1 } else { break }
        // }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "p".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "len".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![
                // Body statement
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "ch".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::FunctionCall {
                        name: "get_char".to_string(),
                        arguments: vec![ASTNode::Variable {
                            name: "p".to_string(),
                            span: Span::unknown(),
                        }],
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
                // The if-else pattern
                ASTNode::If {
                    condition: Box::new(ASTNode::Variable {
                        name: "is_ws".to_string(),
                        span: Span::unknown(),
                    }),
                    then_body: vec![ASTNode::Assignment {
                        target: Box::new(ASTNode::Variable {
                            name: "p".to_string(),
                            span: Span::unknown(),
                        }),
                        value: Box::new(ASTNode::BinaryOp {
                            operator: BinaryOperator::Add,
                            left: Box::new(ASTNode::Variable {
                                name: "p".to_string(),
                                span: Span::unknown(),
                            }),
                            right: Box::new(ASTNode::Literal {
                                value: LiteralValue::Integer(1),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }],
                    else_body: Some(vec![ASTNode::Break {
                        span: Span::unknown(),
                    }]),
                    span: Span::unknown(),
                },
            ],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (skeleton, decision) = result.unwrap();

        // Verify success
        assert!(decision.is_success());
        // Phase 137-5: Pattern choice reflects ExitContract (has_break=true → Pattern2Break)
        assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break));

        // Verify skeleton has Body step
        assert_eq!(skeleton.steps.len(), 3); // HeaderCond + Body + Update
        assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));
        assert!(matches!(skeleton.steps[1], SkeletonStep::Body { .. }));
        assert!(matches!(skeleton.steps[2], SkeletonStep::Update { .. }));

        // Verify body contains 1 statement
        match &skeleton.steps[1] {
            SkeletonStep::Body { stmts } => assert_eq!(stmts.len(), 1),
            _ => panic!("Expected Body step"),
        }
    }

    #[test]
    fn test_skip_whitespace_fails_without_else() {
        // Build pattern without else branch (should fail)
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::Literal {
                value: LiteralValue::Bool(true),
                span: Span::unknown(),
            }),
            body: vec![ASTNode::If {
                condition: Box::new(ASTNode::Literal {
                    value: LiteralValue::Bool(true),
                    span: Span::unknown(),
                }),
                then_body: vec![ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "p".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "p".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(1),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }],
                else_body: None, // No else branch
                span: Span::unknown(),
            }],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (_, decision) = result.unwrap();
        assert!(decision.is_fail_fast());
        assert!(decision.notes[0].contains("Loop does not match"));
        assert!(decision.notes[0].contains("skip_whitespace"));
    }

    #[test]
    fn test_skip_whitespace_fails_with_wrong_delta() {
        // Build pattern with wrong update (p = p * 2, not +/-)
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::Literal {
                value: LiteralValue::Bool(true),
                span: Span::unknown(),
            }),
            body: vec![ASTNode::If {
                condition: Box::new(ASTNode::Literal {
                    value: LiteralValue::Bool(true),
                    span: Span::unknown(),
                }),
                then_body: vec![ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "p".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Multiply, // Wrong operator
                        left: Box::new(ASTNode::Variable {
                            name: "p".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(2),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }],
                else_body: Some(vec![ASTNode::Break {
                    span: Span::unknown(),
                }]),
                span: Span::unknown(),
            }],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (_, decision) = result.unwrap();
        assert!(decision.is_fail_fast());
    }

    #[test]
    fn test_trim_leading_pattern_recognized() {
        // Phase 142 P0: Test trim_leading pattern (start = start + 1)
        // Build: loop(start < end) { if is_ws { start = start + 1 } else { break } }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "start".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "end".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![ASTNode::If {
                condition: Box::new(ASTNode::BinaryOp {
                    operator: BinaryOperator::Equal,
                    left: Box::new(ASTNode::Variable {
                        name: "is_ws".to_string(),
                        span: Span::unknown(),
                    }),
                    right: Box::new(ASTNode::Literal {
                        value: LiteralValue::Integer(1),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }),
                then_body: vec![ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "start".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "start".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(1),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }],
                else_body: Some(vec![ASTNode::Break {
                    span: Span::unknown(),
                }]),
                span: Span::unknown(),
            }],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (skeleton, decision) = result.unwrap();

        // Verify success
        assert!(decision.is_success());
        // chosen == Pattern2Break (ExitContract priority)
        assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break));
        // missing_caps == []
        assert!(decision.missing_caps.is_empty());

        // Verify skeleton structure
        assert_eq!(skeleton.steps.len(), 2); // HeaderCond + Update
        assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));
        assert!(matches!(skeleton.steps[1], SkeletonStep::Update { .. }));

        // Verify carrier
        assert_eq!(skeleton.carriers.len(), 1);
        assert_eq!(skeleton.carriers[0].name, "start");
        assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
        match &skeleton.carriers[0].update_kind {
            UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
            _ => panic!("Expected ConstStep update"),
        }

        // Verify exit contract
        assert!(skeleton.exits.has_break);
        assert!(!skeleton.exits.has_continue);
        assert!(!skeleton.exits.has_return);
    }

    #[test]
    fn test_simple_continue_pattern_recognized() {
        // Phase 142 P1: Test simple continue pattern
        // Build: loop(i < n) { if is_even { i = i + 1; continue } sum = sum + i; i = i + 1 }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "i".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "n".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![
                // if is_even { i = i + 1; continue }
                ASTNode::If {
                    condition: Box::new(ASTNode::Variable {
                        name: "is_even".to_string(),
                        span: Span::unknown(),
                    }),
                    then_body: vec![
                        ASTNode::Assignment {
                            target: Box::new(ASTNode::Variable {
                                name: "i".to_string(),
                                span: Span::unknown(),
                            }),
                            value: Box::new(ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(ASTNode::Variable {
                                    name: "i".to_string(),
                                    span: Span::unknown(),
                                }),
                                right: Box::new(ASTNode::Literal {
                                    value: LiteralValue::Integer(1),
                                    span: Span::unknown(),
                                }),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        },
                        ASTNode::Continue {
                            span: Span::unknown(),
                        },
                    ],
                    else_body: None,
                    span: Span::unknown(),
                },
                // sum = sum + i
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "sum".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "sum".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Variable {
                            name: "i".to_string(),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
                // i = i + 1
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "i".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "i".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(1),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
            ],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (skeleton, decision) = result.unwrap();

        // Verify success
        assert!(decision.is_success());
        // chosen == Pattern4Continue
        assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern4Continue));
        // missing_caps == []
        assert!(decision.missing_caps.is_empty());

        // Verify skeleton structure
        // HeaderCond + Body (sum = sum + i) + Update
        assert!(skeleton.steps.len() >= 2);
        assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));

        // Verify carrier
        assert_eq!(skeleton.carriers.len(), 1);
        assert_eq!(skeleton.carriers[0].name, "i");
        assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
        match &skeleton.carriers[0].update_kind {
            UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
            _ => panic!("Expected ConstStep update"),
        }

        // Verify exit contract
        assert!(!skeleton.exits.has_break);
        assert!(skeleton.exits.has_continue);
        assert!(!skeleton.exits.has_return);
    }

    #[test]
    fn test_trim_trailing_pattern_recognized() {
        // Phase 142 P0: Test trim_trailing pattern (end = end - 1)
        // Build: loop(end > start) { if is_ws { end = end - 1 } else { break } }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Greater,
                left: Box::new(ASTNode::Variable {
                    name: "end".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "start".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![ASTNode::If {
                condition: Box::new(ASTNode::BinaryOp {
                    operator: BinaryOperator::Equal,
                    left: Box::new(ASTNode::Variable {
                        name: "is_ws".to_string(),
                        span: Span::unknown(),
                    }),
                    right: Box::new(ASTNode::Literal {
                        value: LiteralValue::Integer(1),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }),
                then_body: vec![ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "end".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Subtract,
                        left: Box::new(ASTNode::Variable {
                            name: "end".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(1),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                }],
                else_body: Some(vec![ASTNode::Break {
                    span: Span::unknown(),
                }]),
                span: Span::unknown(),
            }],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (skeleton, decision) = result.unwrap();

        // Verify success
        assert!(decision.is_success());
        // chosen == Pattern2Break (ExitContract priority)
        assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break));
        // missing_caps == []
        assert!(decision.missing_caps.is_empty());

        // Verify skeleton structure
        assert_eq!(skeleton.steps.len(), 2); // HeaderCond + Update
        assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));
        assert!(matches!(skeleton.steps[1], SkeletonStep::Update { .. }));

        // Verify carrier
        assert_eq!(skeleton.carriers.len(), 1);
        assert_eq!(skeleton.carriers[0].name, "end");
        assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
        match &skeleton.carriers[0].update_kind {
            UpdateKind::ConstStep { delta } => assert_eq!(*delta, -1), // Negative step
            _ => panic!("Expected ConstStep update"),
        }

        // Verify exit contract
        assert!(skeleton.exits.has_break);
        assert!(!skeleton.exits.has_continue);
        assert!(!skeleton.exits.has_return);
    }

    #[test]
    fn test_parse_string_pattern_recognized() {
        // Phase 143-P1: Test parse_string pattern (both continue AND return)
        // Build: loop(p < len) {
        //   local ch = s.substring(p, p + 1)
        //   if ch == "\"" { return 0 }
        //   if ch == "\\" { p = p + 1; continue }
        //   p = p + 1
        // }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "p".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "len".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![
                // Body statement: local ch = s.substring(p, p + 1)
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "ch".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::FunctionCall {
                        name: "substring".to_string(),
                        arguments: vec![
                            ASTNode::Variable {
                                name: "p".to_string(),
                                span: Span::unknown(),
                            },
                            ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(ASTNode::Variable {
                                    name: "p".to_string(),
                                    span: Span::unknown(),
                                }),
                                right: Box::new(ASTNode::Literal {
                                    value: LiteralValue::Integer(1),
                                    span: Span::unknown(),
                                }),
                                span: Span::unknown(),
                            },
                        ],
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
                // Return check: if ch == "\"" { return 0 }
                ASTNode::If {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Equal,
                        left: Box::new(ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::String("\"".to_string()),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    then_body: vec![ASTNode::Return {
                        value: Some(Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(0),
                            span: Span::unknown(),
                        })),
                        span: Span::unknown(),
                    }],
                    else_body: None,
                    span: Span::unknown(),
                },
                // Escape check: if ch == "\\" { p = p + 1; continue }
                ASTNode::If {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Equal,
                        left: Box::new(ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::String("\\".to_string()),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    then_body: vec![
                        ASTNode::Assignment {
                            target: Box::new(ASTNode::Variable {
                                name: "p".to_string(),
                                span: Span::unknown(),
                            }),
                            value: Box::new(ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(ASTNode::Variable {
                                    name: "p".to_string(),
                                    span: Span::unknown(),
                                }),
                                right: Box::new(ASTNode::Literal {
                                    value: LiteralValue::Integer(1),
                                    span: Span::unknown(),
                                }),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        },
                        ASTNode::Continue {
                            span: Span::unknown(),
                        },
                    ],
                    else_body: None,
                    span: Span::unknown(),
                },
                // Carrier update: p = p + 1
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "p".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "p".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(1),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
            ],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (skeleton, decision) = result.unwrap();

        // Verify success
        assert!(decision.is_success());
        // chosen == Pattern4Continue (has both continue and return)
        assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern4Continue));
        // missing_caps == []
        assert!(decision.missing_caps.is_empty());

        // Verify skeleton structure
        // HeaderCond + Body (ch assignment) + Update
        assert!(skeleton.steps.len() >= 2);
        assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));

        // Verify carrier
        assert_eq!(skeleton.carriers.len(), 1);
        assert_eq!(skeleton.carriers[0].name, "p");
        assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
        match &skeleton.carriers[0].update_kind {
            UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
            _ => panic!("Expected ConstStep update"),
        }

        // Verify exit contract
        assert!(!skeleton.exits.has_break);
        assert!(skeleton.exits.has_continue);
        assert!(skeleton.exits.has_return);
        assert!(!skeleton.exits.break_has_value);
    }

    #[test]
    fn test_parse_array_pattern_recognized() {
        // Phase 143-P2: Test parse_array pattern (both continue AND return)
        // Build: loop(p < len) {
        //   local ch = s.substring(p, p + 1)
        //   if ch == "]" { return 0 }
        //   if ch == "," { p = p + 1; continue }
        //   p = p + 1
        // }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "p".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "len".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![
                // Body statement: local ch = s.substring(p, p + 1)
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "ch".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::FunctionCall {
                        name: "substring".to_string(),
                        arguments: vec![
                            ASTNode::Variable {
                                name: "p".to_string(),
                                span: Span::unknown(),
                            },
                            ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(ASTNode::Variable {
                                    name: "p".to_string(),
                                    span: Span::unknown(),
                                }),
                                right: Box::new(ASTNode::Literal {
                                    value: LiteralValue::Integer(1),
                                    span: Span::unknown(),
                                }),
                                span: Span::unknown(),
                            },
                        ],
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
                // Stop check: if ch == "]" { return 0 }
                ASTNode::If {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Equal,
                        left: Box::new(ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::String("]".to_string()),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    then_body: vec![ASTNode::Return {
                        value: Some(Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(0),
                            span: Span::unknown(),
                        })),
                        span: Span::unknown(),
                    }],
                    else_body: None,
                    span: Span::unknown(),
                },
                // Separator check: if ch == "," { p = p + 1; continue }
                ASTNode::If {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Equal,
                        left: Box::new(ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::String(",".to_string()),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    then_body: vec![
                        ASTNode::Assignment {
                            target: Box::new(ASTNode::Variable {
                                name: "p".to_string(),
                                span: Span::unknown(),
                            }),
                            value: Box::new(ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(ASTNode::Variable {
                                    name: "p".to_string(),
                                    span: Span::unknown(),
                                }),
                                right: Box::new(ASTNode::Literal {
                                    value: LiteralValue::Integer(1),
                                    span: Span::unknown(),
                                }),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        },
                        ASTNode::Continue {
                            span: Span::unknown(),
                        },
                    ],
                    else_body: None,
                    span: Span::unknown(),
                },
                // Regular update: p = p + 1
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "p".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "p".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(1),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
            ],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (skeleton, decision) = result.unwrap();

        // Verify success
        assert!(decision.is_success());
        // chosen == Pattern4Continue (has both continue and return)
        assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern4Continue));
        // missing_caps == []
        assert!(decision.missing_caps.is_empty());

        // Verify skeleton structure
        // HeaderCond + Body (ch assignment) + Update
        assert!(skeleton.steps.len() >= 2);
        assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));

        // Verify carrier
        assert_eq!(skeleton.carriers.len(), 1);
        assert_eq!(skeleton.carriers[0].name, "p");
        assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
        match &skeleton.carriers[0].update_kind {
            UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
            _ => panic!("Expected ConstStep update"),
        }

        // Verify exit contract
        assert!(!skeleton.exits.has_break);
        assert!(skeleton.exits.has_continue);
        assert!(skeleton.exits.has_return);
        assert!(!skeleton.exits.break_has_value);
    }

    #[test]
    fn test_parse_object_pattern_recognized() {
        // Phase 143-P3: Test parse_object pattern (same structure as parse_array)
        // Build: loop(p < len) {
        //   local ch = s.substring(p, p + 1)
        //   if ch == "}" { return 0 }
        //   if ch == "," { p = p + 1; continue }
        //   p = p + 1
        // }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "p".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "len".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![
                // Body statement: local ch = s.substring(p, p + 1)
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "ch".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::FunctionCall {
                        name: "substring".to_string(),
                        arguments: vec![
                            ASTNode::Variable {
                                name: "p".to_string(),
                                span: Span::unknown(),
                            },
                            ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(ASTNode::Variable {
                                    name: "p".to_string(),
                                    span: Span::unknown(),
                                }),
                                right: Box::new(ASTNode::Literal {
                                    value: LiteralValue::Integer(1),
                                    span: Span::unknown(),
                                }),
                                span: Span::unknown(),
                            },
                        ],
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
                // Stop check: if ch == "}" { return 0 }
                ASTNode::If {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Equal,
                        left: Box::new(ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::String("}".to_string()),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    then_body: vec![ASTNode::Return {
                        value: Some(Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(0),
                            span: Span::unknown(),
                        })),
                        span: Span::unknown(),
                    }],
                    else_body: None,
                    span: Span::unknown(),
                },
                // Separator check: if ch == "," { p = p + 1; continue }
                ASTNode::If {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Equal,
                        left: Box::new(ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::String(",".to_string()),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    then_body: vec![
                        ASTNode::Assignment {
                            target: Box::new(ASTNode::Variable {
                                name: "p".to_string(),
                                span: Span::unknown(),
                            }),
                            value: Box::new(ASTNode::BinaryOp {
                                operator: BinaryOperator::Add,
                                left: Box::new(ASTNode::Variable {
                                    name: "p".to_string(),
                                    span: Span::unknown(),
                                }),
                                right: Box::new(ASTNode::Literal {
                                    value: LiteralValue::Integer(1),
                                    span: Span::unknown(),
                                }),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        },
                        ASTNode::Continue {
                            span: Span::unknown(),
                        },
                    ],
                    else_body: None,
                    span: Span::unknown(),
                },
                // Regular update: p = p + 1
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "p".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "p".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(1),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
            ],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (skeleton, decision) = result.unwrap();

        // Verify success
        assert!(decision.is_success());
        // chosen == Pattern4Continue (has both continue and return)
        assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern4Continue));
        // missing_caps == []
        assert!(decision.missing_caps.is_empty());

        // Verify skeleton structure
        // HeaderCond + Body (ch assignment) + Update
        assert!(skeleton.steps.len() >= 2);
        assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));

        // Verify carrier
        assert_eq!(skeleton.carriers.len(), 1);
        assert_eq!(skeleton.carriers[0].name, "p");
        assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
        match &skeleton.carriers[0].update_kind {
            UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
            _ => panic!("Expected ConstStep update"),
        }

        // Verify exit contract
        assert!(!skeleton.exits.has_break);
        assert!(skeleton.exits.has_continue);
        assert!(skeleton.exits.has_return);
        assert!(!skeleton.exits.break_has_value);
    }

    #[test]
    fn test_parse_number_pattern_recognized() {
        // Phase 143-P0: Test parse_number pattern (break in THEN clause)
        // Build: loop(i < len) { digit_pos = digits.indexOf(ch); if digit_pos < 0 { break } result = result + ch; i = i + 1 }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "i".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "len".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![
                // Body statement: digit_pos = digits.indexOf(ch)
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "digit_pos".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::FunctionCall {
                        name: "indexOf".to_string(),
                        arguments: vec![ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }],
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
                // Break check: if digit_pos < 0 { break }
                ASTNode::If {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Less,
                        left: Box::new(ASTNode::Variable {
                            name: "digit_pos".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(0),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    then_body: vec![ASTNode::Break {
                        span: Span::unknown(),
                    }],
                    else_body: None, // No else branch
                    span: Span::unknown(),
                },
                // Rest: result = result + ch
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "result".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "result".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
                // Carrier update: i = i + 1
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "i".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Add,
                        left: Box::new(ASTNode::Variable {
                            name: "i".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::Integer(1),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
            ],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok());

        let (skeleton, decision) = result.unwrap();

        // Verify success
        assert!(decision.is_success());
        // chosen == Pattern2Break (has_break=true)
        assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break));
        // missing_caps == []
        assert!(decision.missing_caps.is_empty());

        // Verify skeleton structure
        // HeaderCond + Body (digit_pos assignment) + Body (result assignment) + Update
        assert!(skeleton.steps.len() >= 3);
        assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));

        // Verify carrier
        assert_eq!(skeleton.carriers.len(), 1);
        assert_eq!(skeleton.carriers[0].name, "i");
        assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
        match &skeleton.carriers[0].update_kind {
            UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
            _ => panic!("Expected ConstStep update"),
        }

        // Verify exit contract
        assert!(skeleton.exits.has_break);
        assert!(!skeleton.exits.has_continue);
        assert!(!skeleton.exits.has_return);
        assert!(!skeleton.exits.break_has_value);
    }

    #[test]
    fn test_escape_skip_pattern_recognition() {
        // Phase 91 P5b: Escape sequence handling pattern
        // Build: loop(i < len) {
        //   ch = get_char(i)
        //   if ch == "\"" { break }
        //   if ch == "\\" { i = i + 2 } else { i = i + 1 }
        // }
        let loop_node = ASTNode::Loop {
            condition: Box::new(ASTNode::BinaryOp {
                operator: BinaryOperator::Less,
                left: Box::new(ASTNode::Variable {
                    name: "i".to_string(),
                    span: Span::unknown(),
                }),
                right: Box::new(ASTNode::Variable {
                    name: "len".to_string(),
                    span: Span::unknown(),
                }),
                span: Span::unknown(),
            }),
            body: vec![
                // Body: ch = get_char(i)
                ASTNode::Assignment {
                    target: Box::new(ASTNode::Variable {
                        name: "ch".to_string(),
                        span: Span::unknown(),
                    }),
                    value: Box::new(ASTNode::FunctionCall {
                        name: "get_char".to_string(),
                        arguments: vec![ASTNode::Variable {
                            name: "i".to_string(),
                            span: Span::unknown(),
                        }],
                        span: Span::unknown(),
                    }),
                    span: Span::unknown(),
                },
                // Break check: if ch == "\"" { break }
                ASTNode::If {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Equal,
                        left: Box::new(ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::String("\"".to_string()),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    then_body: vec![ASTNode::Break {
                        span: Span::unknown(),
                    }],
                    else_body: None,
                    span: Span::unknown(),
                },
                // Escape check: if ch == "\\" { i = i + 2 } else { i = i + 1 }
                ASTNode::If {
                    condition: Box::new(ASTNode::BinaryOp {
                        operator: BinaryOperator::Equal,
                        left: Box::new(ASTNode::Variable {
                            name: "ch".to_string(),
                            span: Span::unknown(),
                        }),
                        right: Box::new(ASTNode::Literal {
                            value: LiteralValue::String("\\".to_string()),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }),
                    then_body: vec![ASTNode::Assignment {
                        target: Box::new(ASTNode::Variable {
                            name: "i".to_string(),
                            span: Span::unknown(),
                        }),
                        value: Box::new(ASTNode::BinaryOp {
                            operator: BinaryOperator::Add,
                            left: Box::new(ASTNode::Variable {
                                name: "i".to_string(),
                                span: Span::unknown(),
                            }),
                            right: Box::new(ASTNode::Literal {
                                value: LiteralValue::Integer(2),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }],
                    else_body: Some(vec![ASTNode::Assignment {
                        target: Box::new(ASTNode::Variable {
                            name: "i".to_string(),
                            span: Span::unknown(),
                        }),
                        value: Box::new(ASTNode::BinaryOp {
                            operator: BinaryOperator::Add,
                            left: Box::new(ASTNode::Variable {
                                name: "i".to_string(),
                                span: Span::unknown(),
                            }),
                            right: Box::new(ASTNode::Literal {
                                value: LiteralValue::Integer(1),
                                span: Span::unknown(),
                            }),
                            span: Span::unknown(),
                        }),
                        span: Span::unknown(),
                    }]),
                    span: Span::unknown(),
                },
            ],
            span: Span::unknown(),
        };

        let result = canonicalize_loop_expr(&loop_node);
        assert!(result.is_ok(), "Escape pattern canonicalization should succeed");

        let (skeleton, decision) = result.unwrap();

        // Verify decision success
        assert!(decision.is_success(), "Decision should indicate success");
        assert_eq!(
            decision.chosen,
            Some(LoopPatternKind::Pattern2Break),
            "P5b should route to Pattern2Break (has_break=true)"
        );
        assert!(decision.missing_caps.is_empty(), "No missing capabilities");

        // Verify skeleton structure
        // Expected: HeaderCond + Body + Update
        assert!(
            skeleton.steps.len() >= 3,
            "Expected at least 3 steps: HeaderCond, Body, Update"
        );
        assert!(
            matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }),
            "First step should be HeaderCond"
        );
        assert!(
            matches!(skeleton.steps[skeleton.steps.len() - 1], SkeletonStep::Update { .. }),
            "Last step should be Update"
        );

        // Verify carrier (counter variable "i")
        assert_eq!(skeleton.carriers.len(), 1, "Should have 1 carrier");
        let carrier = &skeleton.carriers[0];
        assert_eq!(carrier.name, "i", "Carrier should be named 'i'");
        assert_eq!(carrier.role, CarrierRole::Counter, "Carrier should be a Counter");

        // Verify ConditionalStep with escape_delta=2, normal_delta=1
        // Phase 92 P0-3: ConditionalStep now includes cond
        match &carrier.update_kind {
            UpdateKind::ConditionalStep {
                cond: _,  // Phase 92 P0-3: Condition for Select (don't check exact AST)
                then_delta,
                else_delta,
            } => {
                assert_eq!(*then_delta, 2, "Escape delta (then) should be 2");
                assert_eq!(*else_delta, 1, "Normal delta (else) should be 1");
            }
            other => panic!(
                "Expected ConditionalStep, got {:?}",
                other
            ),
        }

        // Verify exit contract (P5b has break for string boundary)
        assert!(skeleton.exits.has_break, "P5b should have break");
        assert!(!skeleton.exits.has_continue, "P5b should not have continue");
        assert!(!skeleton.exits.has_return, "P5b should not have return");
        assert!(!skeleton.exits.break_has_value, "Break should not have value");
    }
}
