// Unified Native Region Scanner for Frame V4
//
// This module provides a single scanning implementation for all target languages.
// The only language-specific logic is how to skip comments and strings.
//
// Sub-machines (hierarchical state manager pattern):
//   ExprScannerFsm   — PDA for scanning assignment RHS expressions
//   ContextParserFsm — FSM for parsing @@ context constructs
//   StateVarParserFsm — FSM for parsing $.varName access/assignment

#[allow(unreachable_patterns)]
#[allow(unused_mut)]
#[allow(dead_code)]
#[allow(non_snake_case)]
#[allow(unused_variables)]
mod _expr_scanner {
    include!("expr_scanner.gen.rs");
}

#[allow(unreachable_patterns)]
#[allow(unused_mut)]
#[allow(dead_code)]
#[allow(non_snake_case)]
#[allow(unused_variables)]
mod _context_parser {
    include!("context_parser.gen.rs");
}

#[allow(unreachable_patterns)]
#[allow(unused_mut)]
#[allow(dead_code)]
#[allow(non_snake_case)]
#[allow(unused_variables)]
mod _state_var_parser {
    include!("state_var_parser.gen.rs");
}

mod interpolation;
mod metadata;

pub use interpolation::{
    scan_dollar_string_regions, scan_fstring_regions, scan_hash_string_regions,
    scan_paren_string_regions, scan_template_literal_regions,
};

use _context_parser::ContextParserFsm;
use _state_var_parser::StateVarParserFsm;
use metadata::extract_segment_metadata;

use super::*;
use crate::frame_c::compiler::body_closer::BodyCloser;

/// A region inside a string interpolation expression that may contain
/// Frame constructs (`$.varName`, `@@:`). The main scanner scans these
/// regions for Frame constructs while skipping the surrounding string
/// content.
#[derive(Debug, Clone)]
pub struct InterpRegion {
    /// First byte of the expression (after `{`, `${`, `#{`, `\(`, etc.)
    pub start: usize,
    /// Position of the closing delimiter (`}`, `)`, etc.) — exclusive
    pub end: usize,
    /// The string delimiter character (`"`, `'`, or `` ` ``).
    /// The expander uses the opposite quote for dict keys to avoid
    /// quote collisions inside string interpolation.
    pub quote: u8,
}

/// Language-specific syntax skipper trait.
/// Each language only needs to implement how to skip its comments and strings.
pub trait SyntaxSkipper {
    /// Get the body closer for this language
    fn body_closer(&self) -> Box<dyn BodyCloser>;

    /// Try to skip a comment starting at position i.
    /// Returns Some(new_position) if a comment was skipped, None otherwise.
    fn skip_comment(&self, bytes: &[u8], i: usize, end: usize) -> Option<usize>;

    /// Try to skip a string literal starting at position i.
    /// Returns Some(new_position) if a string was skipped, None otherwise.
    fn skip_string(&self, bytes: &[u8], i: usize, end: usize) -> Option<usize>;

    /// Find the end of a Frame statement line, respecting language-specific string syntax.
    /// Stops at newline, semicolon, or comment start.
    fn find_line_end(&self, bytes: &[u8], start: usize, end: usize) -> usize;

    /// Try to find matching close paren, respecting language-specific string syntax.
    /// Returns Some(position after ')') if balanced, None otherwise.
    fn balanced_paren_end(&self, bytes: &[u8], i: usize, end: usize) -> Option<usize>;

    /// Try to skip a nested function scope (closure/lambda) starting at position i.
    /// Returns Some(position after scope end) if a nested scope was found and skipped.
    /// Used to detect closures that would trap Frame statement return values.
    /// Default: None (no nested scope detection).
    fn skip_nested_scope(&self, _bytes: &[u8], _i: usize, _end: usize) -> Option<usize> {
        None
    }

    /// Try to scan an interpolated string starting at position i.
    /// Returns Some((end_of_string, regions)) where regions mark
    /// interpolation expressions that may contain Frame constructs.
    /// Returns None if not positioned at an interpolated string.
    /// Default: None (language has no string interpolation).
    fn string_interp_regions(
        &self,
        _bytes: &[u8],
        _i: usize,
        _end: usize,
    ) -> Option<(usize, Vec<InterpRegion>)> {
        None
    }
}

/// Unified scanner that works with any language via the SyntaxSkipper trait
pub fn scan_native_regions<S: SyntaxSkipper>(
    skipper: &S,
    bytes: &[u8],
    open_brace_index: usize,
) -> Result<ScanResult, ScanError> {
    let mut closer = skipper.body_closer();
    let close = closer
        .close_byte(bytes, open_brace_index)
        .map_err(|e| ScanError {
            kind: ScanErrorKind::UnterminatedProtected,
            message: format!("{:?}", e),
        })?;

    let mut regions: Vec<Region> = Vec::new();
    let mut i = open_brace_index + 1;
    let end = close;
    let mut seg_start = i;

    while i < end {
        let b = bytes[i];

        // ===== NESTED SCOPE DETECTION =====
        // Skip closures/lambdas that would trap Frame statement return values.
        // Frame statements inside nested scopes are rejected with E407.
        if let Some(scope_end) = skipper.skip_nested_scope(bytes, i, end) {
            // Check if the skipped scope contains any Frame statement
            // markers. We require the trailing `$` so that target-
            // language `->` forms (Erlang `fun() -> Expr end`, Java
            // method references `obj::method`, Rust function pointer
            // returns `fn() -> T`, etc.) don't trip a false positive
            // on the bare `-> ` byte sequence — only `-> $` and
            // `=> $` are Frame transition / forward syntax.
            let scope_bytes = &bytes[i..scope_end];
            let has_frame_stmt = scope_bytes.windows(4).any(|w| {
                // `-> $X` (transition)
                (w[0] == b'-' && w[1] == b'>' && w[2] == b' ' && w[3] == b'$')
                // `=> $X` (forward)
                || (w[0] == b'=' && w[1] == b'>' && w[2] == b' ' && w[3] == b'$')
            }) || scope_bytes.windows(5).any(|w| {
                // push$
                (w[0] == b'p' && w[1] == b'u' && w[2] == b's' && w[3] == b'h' && w[4] == b'$')
                // pop$ (as standalone, not -> pop$)
                || (w[0] == b' ' && w[1] == b'p' && w[2] == b'o' && w[3] == b'p' && w[4] == b'$')
            });

            if has_frame_stmt {
                return Err(ScanError {
                    kind: ScanErrorKind::UnterminatedProtected,
                    message: "E407: Frame statement (transition, forward, push, pop) inside nested function scope. \
                              Frame control-flow statements must be directly in event handler scope, \
                              not inside closures or nested functions.".to_string(),
                });
            }

            // No Frame statements — skip the scope as native text
            i = scope_end;
            continue;
        }

        // ===== FRAME STATEMENT DETECTION =====
        // Frame statements (-> $, => $, push$, pop$, return) are detected at any position.
        // Closures are already skipped by skip_nested_scope() above.
        if matches!(b, b'-' | b'=' | b'(' | b'p' | b'r') {
            if let Some((match_pos, kind)) = match_frame_statement(skipper, bytes, i, end) {
                // Calculate indent: count leading whitespace from start of
                // current line.
                //
                // When the Frame statement follows native code on the same
                // line, we have to distinguish two cases:
                //
                //   1. Native is a block-opener (`if cond:`, `if (c) {`,
                //      `for (…) {`). The Frame token forms the one-line
                //      block body — indent one level deeper.
                //
                //   2. Native is a plain statement (`self.x = 0;`,
                //      `f();`, `x = y`). The Frame token is a successor
                //      statement — same indent as the native.
                //
                // We pick between the two by looking at the last non-
                // whitespace byte of the native text: `:` and `{` open a
                // block in every target language we support; anything else
                // (`;`, `)`, identifier char, `=`, …) is a statement
                // terminator or value continuation.
                let mut line_start = i;
                while line_start > open_brace_index + 1 && bytes[line_start - 1] != b'\n' {
                    line_start -= 1;
                }
                let leading_ws = bytes[line_start..i]
                    .iter()
                    .take_while(|&&b| b == b' ' || b == b'\t')
                    .count();
                let frame_at_sol = leading_ws == (i - line_start);
                // Find last non-whitespace byte between line_start and i.
                let mut last_non_ws: Option<u8> = None;
                for b in bytes[line_start..i].iter().rev() {
                    if *b != b' ' && *b != b'\t' {
                        last_non_ws = Some(*b);
                        break;
                    }
                }
                let native_opens_block = matches!(last_non_ws, Some(b':') | Some(b'{'));
                let indent = if frame_at_sol {
                    i - line_start // Frame token starts the line
                } else if native_opens_block {
                    leading_ws + 4 // Frame is the block body — one level deeper
                } else {
                    leading_ws // Frame is a successor statement — same indent
                };

                // Emit any preceding native text
                if seg_start < i {
                    // Trim trailing whitespace from native text (it's indentation for the Frame statement)
                    let mut native_end = i;
                    while native_end > seg_start
                        && (bytes[native_end - 1] == b' ' || bytes[native_end - 1] == b'\t')
                    {
                        native_end -= 1;
                    }
                    if seg_start < native_end {
                        regions.push(Region::NativeText {
                            span: RegionSpan {
                                start: seg_start,
                                end: native_end,
                            },
                        });
                    }
                }

                // E400 (FRAMEC_BUGS #13): a transition must be the last
                // statement on its line. Inline native control flow with
                // transition arms — `if c then -> $A else -> $B end` — can't
                // scope the transition's implicit `return` through an opaque
                // native block (Oceans Model), so it silently miscompiles
                // (wrong arm taken, dropped `else`, or a broken `end`). Reject
                // it; the multi-line if/then/else form works correctly.
                //
                // Allowed to follow the transition on its line: whitespace, a
                // comment, a block/terminator delimiter (`}` / `;`), or a
                // Frame continuation (`@`). Anything else (`else`, `end`, a
                // second `->`, …) means the transition isn't the last token.
                if kind == FrameSegmentKind::Transition {
                    let construct_end = if match_pos < end && bytes[match_pos] == b'$' {
                        let mut k = match_pos + 1;
                        if k < end && bytes[k] == b'^' {
                            k += 1; // $^
                        } else {
                            while k < end && (bytes[k].is_ascii_alphanumeric() || bytes[k] == b'_')
                            {
                                k += 1;
                            }
                        }
                        if k < end && bytes[k] == b'(' {
                            if let Some(k2) = skipper.balanced_paren_end(bytes, k, end) {
                                k = k2;
                            }
                        }
                        k
                    } else {
                        match_pos // `-> pop$` etc — already past the construct
                    };
                    let after = skip_ws(bytes, construct_end, end);
                    let trailing_code = after < end
                        && !matches!(bytes[after], b'\n' | b'\r' | b'}' | b';' | b'@')
                        && skipper.skip_comment(bytes, after, end).is_none();
                    if trailing_code {
                        return Err(ScanError {
                            kind: ScanErrorKind::UnterminatedProtected,
                            message: "E400: a transition must be the last statement on its line. \
                                      Inline native control flow with transition arms (e.g. \
                                      `if c then -> $A else -> $B end`) is not supported — the \
                                      transition's implicit return cannot be scoped through an \
                                      opaque native block. Put each transition on its own line \
                                      using the multi-line form."
                                .to_string(),
                        });
                    }
                }

                // Find end of Frame statement. Seed the line-end search from
                // the matched construct position rather than `i`: when a
                // transition spans a newline (`->` <newline> `$State`,
                // FRAMEC_BUGS #43) the target lives on a later line, so the
                // statement end is that line's end — not the `->` line's.
                // For all same-line constructs `match_pos` is on the same line
                // as `i`, so this is identical to `find_line_end(.., i, ..)`.
                let raw_stmt_end = skipper.find_line_end(bytes, match_pos.max(i), end);
                // Trim trailing inline whitespace from the Frame segment
                // span so it stays in the *following* NativeText region.
                // Without this, a same-line trailing comment after a Frame
                // statement (`-> $B  # note`) emits as `return# note` —
                // valid but ugly — because `find_line_end` stops at the
                // `#` so the two spaces between the statement and the
                // comment land inside the segment span and get consumed
                // by the multi-line expansion.
                let mut stmt_end = raw_stmt_end;
                while stmt_end > i && (bytes[stmt_end - 1] == b' ' || bytes[stmt_end - 1] == b'\t')
                {
                    stmt_end -= 1;
                }

                let stmt_bytes = &bytes[i..stmt_end];
                let stmt_text = String::from_utf8_lossy(stmt_bytes);
                let metadata = extract_segment_metadata(kind, &stmt_text);

                regions.push(Region::FrameSegment {
                    span: RegionSpan {
                        start: i,
                        end: stmt_end,
                    },
                    kind,
                    indent,
                    metadata,
                });

                i = stmt_end;
                seg_start = i;
                continue;
            }
        }

        // ===== HANDLE CURRENT CHARACTER =====

        match b {
            b'\n' => {
                i += 1;
            }

            // Try language-specific comment skip
            _ if skipper.skip_comment(bytes, i, end).is_some() => {
                // Safe: is_some() guard guarantees Some
                i = skipper.skip_comment(bytes, i, end).unwrap_or(i + 1);
            }

            // Try interpolation-aware string scan first (RFC-0010).
            // If the string contains interpolation expressions, scan
            // them for Frame constructs while skipping string content.
            _ if skipper.string_interp_regions(bytes, i, end).is_some() => {
                let (str_end, interp_regions) = skipper
                    .string_interp_regions(bytes, i, end)
                    .expect("match guard verified is_some() one line above");
                if interp_regions.is_empty() {
                    // No interpolation regions — skip entire string
                    i = str_end;
                } else {
                    // Emit native text up to string start (already in seg_start..i)
                    // Then process the string: string content is native text,
                    // interpolation regions are scanned for Frame constructs.
                    // The entire string (including interpolation) is emitted as
                    // one NativeText region — the Frame constructs within
                    // interpolation expressions are spliced in.
                    //
                    // Strategy: recurse the $./@@ detection only on the
                    // interpolation regions. Everything else passes through.
                    let mut has_frame_construct = false;
                    for region in &interp_regions {
                        for k in region.start..region.end {
                            if k + 1 < region.end && bytes[k] == b'$' && bytes[k + 1] == b'.' {
                                has_frame_construct = true;
                                break;
                            }
                            if k + 1 < region.end && bytes[k] == b'@' && bytes[k + 1] == b'@' {
                                has_frame_construct = true;
                                break;
                            }
                        }
                        if has_frame_construct {
                            break;
                        }
                    }
                    if !has_frame_construct {
                        // Interpolation exists but no Frame constructs — skip
                        i = str_end;
                    } else {
                        // Emit native text before each Frame construct region,
                        // let the main loop's $./@@ detection handle the rest.
                        // We do this by NOT skipping — emit the string content
                        // up to the first interp region as native, then let
                        // the main loop process from the interp region start.
                        //
                        // Emit native text before the interpolation region
                        if seg_start < interp_regions[0].start {
                            regions.push(Region::NativeText {
                                span: RegionSpan {
                                    start: seg_start,
                                    end: interp_regions[0].start,
                                },
                            });
                        }
                        // Process each interpolation region — let the inner
                        // content be scanned by a recursive call to the
                        // $./@@ detection logic. Between regions, emit native.
                        let mut prev_end = interp_regions[0].start;
                        for region in &interp_regions {
                            // Native text between previous region end and this region start
                            // (includes interpolation delimiters: }...{)
                            if prev_end < region.start {
                                regions.push(Region::NativeText {
                                    span: RegionSpan {
                                        start: prev_end,
                                        end: region.start,
                                    },
                                });
                            }
                            prev_end = region.start;
                            // Scan the interpolation region for $./@@ constructs
                            // by running the inner scanner on just these bytes
                            let mut inner_pos = region.start;
                            while inner_pos < region.end {
                                if inner_pos + 1 < region.end
                                    && bytes[inner_pos] == b'$'
                                    && bytes[inner_pos + 1] == b'.'
                                {
                                    // Emit native text before $.
                                    if prev_end < inner_pos {
                                        regions.push(Region::NativeText {
                                            span: RegionSpan {
                                                start: prev_end,
                                                end: inner_pos,
                                            },
                                        });
                                    }
                                    // Parse the state variable
                                    let var_start = inner_pos;
                                    let mut parser = StateVarParserFsm::new();
                                    parser.bytes = bytes[..region.end].to_vec();
                                    parser.pos = inner_pos;
                                    parser.end = region.end;
                                    parser.do_parse();

                                    let kind = if parser.is_assignment {
                                        FrameSegmentKind::StateVarAssign
                                    } else {
                                        FrameSegmentKind::StateVar
                                    };
                                    let sv_bytes = &bytes[var_start..parser.result_end];
                                    let sv_text = String::from_utf8_lossy(sv_bytes);
                                    let mut metadata = extract_segment_metadata(kind, &sv_text);
                                    // Carry the string delimiter so the expander
                                    // uses the opposite quote for dict keys
                                    if let SegmentMetadata::StateVar {
                                        ref mut interp_quote,
                                        ..
                                    } = metadata
                                    {
                                        *interp_quote = Some(region.quote);
                                    }

                                    regions.push(Region::FrameSegment {
                                        span: RegionSpan {
                                            start: var_start,
                                            end: parser.result_end,
                                        },
                                        kind,
                                        indent: 0,
                                        metadata,
                                    });
                                    inner_pos = parser.result_end;
                                    prev_end = inner_pos;
                                } else if inner_pos + 1 < region.end
                                    && bytes[inner_pos] == b'@'
                                    && bytes[inner_pos + 1] == b'@'
                                {
                                    // @@ construct inside interpolation — emit
                                    // native text before it, then handle @@
                                    if prev_end < inner_pos {
                                        regions.push(Region::NativeText {
                                            span: RegionSpan {
                                                start: prev_end,
                                                end: inner_pos,
                                            },
                                        });
                                    }
                                    // For now, emit @@ content as native text
                                    // (full @@ parsing in interpolation is rare)
                                    inner_pos += 2;
                                    prev_end = inner_pos;
                                } else {
                                    inner_pos += 1;
                                }
                            }
                            // Emit remaining region content not consumed
                            // by Frame construct detection (handles regions
                            // like {total} that contain no $./@@ at all)
                            if prev_end < region.end {
                                regions.push(Region::NativeText {
                                    span: RegionSpan {
                                        start: prev_end,
                                        end: region.end,
                                    },
                                });
                            }
                            prev_end = region.end;
                        }
                        // Native text from last region end to string end
                        if prev_end < str_end {
                            regions.push(Region::NativeText {
                                span: RegionSpan {
                                    start: prev_end,
                                    end: str_end,
                                },
                            });
                        }
                        i = str_end;
                        seg_start = i;
                    }
                }
            }

            // Try language-specific string skip (non-interpolated strings)
            _ if skipper.skip_string(bytes, i, end).is_some() => {
                // Safe: is_some() guard guarantees Some
                i = skipper.skip_string(bytes, i, end).unwrap_or(i + 1);
            }

            // ===== MID-LINE FRAME CONSTRUCTS =====

            // State variable reference: $.varName or assignment: $.varName = expr
            // Delegates to StateVarParserFsm (hierarchical state manager pattern)
            b'$' if i + 1 < end && bytes[i + 1] == b'.' => {
                if seg_start < i {
                    regions.push(Region::NativeText {
                        span: RegionSpan {
                            start: seg_start,
                            end: i,
                        },
                    });
                }
                let var_start = i;
                let mut parser = StateVarParserFsm::new();
                parser.bytes = bytes[..end].to_vec();
                parser.pos = i;
                parser.end = end;
                parser.do_parse();

                let kind = if parser.is_assignment {
                    FrameSegmentKind::StateVarAssign
                } else {
                    FrameSegmentKind::StateVar
                };
                let sv_bytes = &bytes[var_start..parser.result_end];
                let sv_text = String::from_utf8_lossy(sv_bytes);
                let metadata = extract_segment_metadata(kind, &sv_text);

                regions.push(Region::FrameSegment {
                    span: RegionSpan {
                        start: var_start,
                        end: parser.result_end,
                    },
                    kind,
                    indent: 0,
                    metadata,
                });
                i = parser.result_end;
                seg_start = i;
                // parser is destroyed here (state manager pattern)
            }

            // System context syntax: @@ variants
            // Delegates to ContextParserFsm (hierarchical state manager pattern)
            b'@' if i + 1 < end && bytes[i + 1] == b'@' => {
                // Defer native text emission until after computed_indent
                // is known — standalone @@ constructs (indent > 0) trim
                // trailing whitespace, inline ones (indent = 0) don't.
                let ctx_start = i;

                // For @@SystemName() and @@!SystemName(), pre-compute the
                // balanced_paren_end via the language-specific SyntaxSkipper
                // (the FSM can't call traits). For the `@@!` case, the system
                // name starts one byte further in.
                let after_at = i + 2;
                let name_start = if after_at < end && bytes[after_at] == b'!' {
                    after_at + 1
                } else {
                    after_at
                };
                let mut precomputed_paren_end: usize = 0;
                if name_start < end && bytes[name_start].is_ascii_uppercase() {
                    let mut name_end = name_start;
                    while name_end < end
                        && (bytes[name_end].is_ascii_alphanumeric() || bytes[name_end] == b'_')
                    {
                        name_end += 1;
                    }
                    if name_end < end && bytes[name_end] == b'(' {
                        if let Some(pe) = skipper.balanced_paren_end(bytes, name_end, end) {
                            precomputed_paren_end = pe;
                        }
                    }
                }

                let mut parser = ContextParserFsm::new();
                parser.bytes = bytes[..end].to_vec();
                parser.pos = after_at; // position after @@
                parser.end = end;
                parser.paren_end = precomputed_paren_end;
                parser.do_parse();

                if parser.has_result {
                    let kind = match parser.result_kind {
                        2 => FrameSegmentKind::ContextReturn,
                        3 => FrameSegmentKind::ContextEvent,
                        4 => FrameSegmentKind::ContextData,
                        5 => FrameSegmentKind::ContextDataAssign,
                        6 => FrameSegmentKind::ContextParams,
                        7 => FrameSegmentKind::SystemInstantiation,
                        8 => FrameSegmentKind::ContextReturnExpr,
                        9 => FrameSegmentKind::ReturnCall,
                        10 => FrameSegmentKind::ContextSelfCall,
                        11 => FrameSegmentKind::ContextSelf,
                        12 => FrameSegmentKind::ContextSystemState,
                        13 => FrameSegmentKind::ContextSystemBare,
                        14 => FrameSegmentKind::ContextSystemStateReserved,
                        _ => FrameSegmentKind::ContextReturn, // shouldn't happen
                    };
                    let mut seg_end = parser.result_end;

                    // For `@@:(expr) return;` on the same source line,
                    // consume the trailing whitespace + `return` + optional `;`
                    // as part of the ContextReturnExpr segment. Without
                    // this the splicer leaves them as separate native
                    // text on the same line as the assignment, which
                    // most languages reject ("two statements on one
                    // line"). The expansion will emit both pieces with
                    // proper indentation.
                    if kind == FrameSegmentKind::ContextReturnExpr {
                        let mut p = seg_end;
                        // Skip trailing inline whitespace.
                        while p < end && (bytes[p] == b' ' || bytes[p] == b'\t') {
                            p += 1;
                        }
                        // Match `return` as a whole word.
                        if p + 6 <= end && &bytes[p..p + 6] == b"return" {
                            let after_return = p + 6;
                            // Make sure `return` is a whole word (not
                            // followed by an identifier character).
                            let is_word_boundary = after_return >= end
                                || !(bytes[after_return].is_ascii_alphanumeric()
                                    || bytes[after_return] == b'_');
                            if is_word_boundary {
                                let mut q = after_return;
                                // Skip whitespace after `return`.
                                while q < end && (bytes[q] == b' ' || bytes[q] == b'\t') {
                                    q += 1;
                                }
                                // Optional trailing `;`.
                                if q < end && bytes[q] == b';' {
                                    q += 1;
                                }
                                seg_end = q;
                            }
                        }
                    }

                    // Compute indent ONLY for ContextReturnExpr because
                    // its expansion uses it to indent the trailing
                    // `return` statement (the one introduced when the
                    // scanner consumed `@@:(expr) return;` as a single
                    // segment). All other @@ segments leave indent at 0
                    // because their expansions emit a single statement
                    // and the splicer's leading native text already
                    // carries the correct indentation.
                    let computed_indent = if kind == FrameSegmentKind::ContextReturnExpr
                        || kind == FrameSegmentKind::ReturnCall
                        || kind == FrameSegmentKind::ContextSelfCall
                    {
                        // Find the start of this line
                        let mut line_start = ctx_start;
                        while line_start > open_brace_index + 1 && bytes[line_start - 1] != b'\n' {
                            line_start -= 1;
                        }
                        if kind == FrameSegmentKind::ContextSelfCall {
                            // Always use line's leading whitespace — the guard
                            // needs correct indent regardless of standalone vs inline.
                            // The expansion uses indent > 0 to decide whether to
                            // prefix indent_str on the call line.
                            let mut ws = 0;
                            let mut p = line_start;
                            while p < ctx_start && (bytes[p] == b' ' || bytes[p] == b'\t') {
                                ws += 1;
                                p += 1;
                            }
                            ws
                        } else {
                            ctx_start - line_start
                        }
                    } else {
                        0
                    };

                    // Emit deferred native text. Trim trailing whitespace
                    // only when the construct is STANDALONE on its line —
                    // i.e. everything between the previous newline and
                    // `ctx_start` is whitespace. In that case the expansion
                    // supplies its own leading indent via `indent_str`, so
                    // we must strip the preceding whitespace to avoid
                    // double-indentation. For INLINE usage (e.g. Swift's
                    // `let x = @@:self.method()`) the preceding native
                    // text contains a real expression; trimming here would
                    // collapse `= @@:self` → `=self`, breaking
                    // whitespace-sensitive languages. Detect by walking
                    // back to the previous newline: if any non-whitespace
                    // byte is seen before then, we're inline.
                    if seg_start < ctx_start {
                        let standalone = computed_indent > 0 && {
                            let mut p = ctx_start;
                            let mut only_ws = true;
                            while p > seg_start && bytes[p - 1] != b'\n' {
                                let b = bytes[p - 1];
                                if b != b' ' && b != b'\t' {
                                    only_ws = false;
                                    break;
                                }
                                p -= 1;
                            }
                            only_ws
                        };
                        let native_end = if standalone {
                            let mut ne = ctx_start;
                            while ne > seg_start
                                && (bytes[ne - 1] == b' ' || bytes[ne - 1] == b'\t')
                            {
                                ne -= 1;
                            }
                            ne
                        } else {
                            ctx_start
                        };
                        if seg_start < native_end {
                            regions.push(Region::NativeText {
                                span: RegionSpan {
                                    start: seg_start,
                                    end: native_end,
                                },
                            });
                        }
                    }

                    let segment_bytes = &bytes[ctx_start..seg_end];
                    let segment_text = String::from_utf8_lossy(segment_bytes);
                    let metadata = extract_segment_metadata(kind, &segment_text);

                    regions.push(Region::FrameSegment {
                        span: RegionSpan {
                            start: ctx_start,
                            end: seg_end,
                        },
                        kind,
                        indent: computed_indent,
                        metadata,
                    });
                    i = seg_end;
                } else {
                    // No match — emit deferred native text + this token
                    if seg_start < ctx_start {
                        regions.push(Region::NativeText {
                            span: RegionSpan {
                                start: seg_start,
                                end: ctx_start,
                            },
                        });
                    }
                    regions.push(Region::NativeText {
                        span: RegionSpan {
                            start: ctx_start,
                            end: parser.result_end,
                        },
                    });
                    i = parser.result_end;
                }
                seg_start = i;
                // parser is destroyed here (state manager pattern)
            }

            _ => {
                i += 1;
            }
        }
    }

    // Emit any remaining native text
    if seg_start < end {
        regions.push(Region::NativeText {
            span: RegionSpan {
                start: seg_start,
                end,
            },
        });
    }

    Ok(ScanResult {
        close_byte: close,
        regions,
    })
}

/// Match Frame statements at start of line.
/// Returns Some((new_position, kind)) if matched, None otherwise.
///
/// Handles both:
/// - Direct Frame statements: `-> $State`, `push$`, etc.
/// Match a Frame statement at the given position.
/// Frame statements are unambiguous token sequences that can be detected
/// at any position in a handler body (not just start-of-line).
fn match_frame_statement<S: SyntaxSkipper>(
    skipper: &S,
    bytes: &[u8],
    i: usize,
    end: usize,
) -> Option<(usize, FrameSegmentKind)> {
    let pos = i;

    if pos >= end {
        return None;
    }

    let b = bytes[pos];

    // Transition variants: -> $State, -> (args) $State, -> pop$, -> => $State
    if b == b'-' && pos + 1 < end && bytes[pos + 1] == b'>' {
        // Newline-aware: a transition may be written `->` <newline> `$State`
        // (FRAMEC_BUGS #43). The trailing-`$` requirement below still keeps
        // native `->` forms (Rust `-> T`, Erlang `-> Expr`) from matching.
        let mut k = skip_ws_nl(bytes, pos + 2, end);

        // Check for -> => $State (transition forward)
        if k + 1 < end && bytes[k] == b'=' && bytes[k + 1] == b'>' {
            k = skip_ws_nl(bytes, k + 2, end);
            if k < end && bytes[k] == b'$' {
                return Some((k, FrameSegmentKind::Transition));
            }
        }

        // Check for -> pop$ (pop transition — this IS a transition, not standalone pop)
        if k + 3 < end
            && bytes[k] == b'p'
            && bytes[k + 1] == b'o'
            && bytes[k + 2] == b'p'
            && bytes[k + 3] == b'$'
        {
            return Some((k + 4, FrameSegmentKind::Transition));
        }

        // Check for optional enter args: -> (args) $State
        if k < end && bytes[k] == b'(' {
            if let Some(k2) = skipper.balanced_paren_end(bytes, k, end) {
                k = skip_ws_nl(bytes, k2, end);
            }
        }

        // Check for optional label: -> "label" $State or -> (args) "label" $State
        if k < end && (bytes[k] == b'"' || bytes[k] == b'\'') {
            let quote = bytes[k];
            k += 1;
            while k < end && bytes[k] != quote {
                if bytes[k] == b'\\' && k + 1 < end {
                    k += 2;
                } else {
                    k += 1;
                }
            }
            if k < end {
                k += 1;
            } // Skip closing quote
            k = skip_ws_nl(bytes, k, end);
        }

        // Regular transition: -> $State
        if k < end && bytes[k] == b'$' {
            return Some((k, FrameSegmentKind::Transition));
        }

        // -> (args) pop$ — enter args on pop transition
        if k + 3 < end
            && bytes[k] == b'p'
            && bytes[k + 1] == b'o'
            && bytes[k + 2] == b'p'
            && bytes[k + 3] == b'$'
        {
            return Some((k + 4, FrameSegmentKind::Transition));
        }
    }

    // Forward: => $^  (with any whitespace between => and $)
    if b == b'=' && pos + 1 < end && bytes[pos + 1] == b'>' {
        let k = skip_ws(bytes, pos + 2, end);
        if k < end && bytes[k] == b'$' {
            return Some((k, FrameSegmentKind::Forward));
        }
    }

    // Transition with leading exit args: (exit_args) -> (enter_args) $State
    if b == b'(' {
        if let Some(k) = skipper.balanced_paren_end(bytes, pos, end) {
            let mut k = skip_ws(bytes, k, end);
            if k + 1 < end && bytes[k] == b'-' && bytes[k + 1] == b'>' {
                k = skip_ws(bytes, k + 2, end);
                // Optional enter args
                if k < end && bytes[k] == b'(' {
                    if let Some(k2) = skipper.balanced_paren_end(bytes, k, end) {
                        k = skip_ws(bytes, k2, end);
                    }
                }
                if k < end && bytes[k] == b'$' {
                    return Some((k, FrameSegmentKind::Transition));
                }
                // (exit) -> (enter)? pop$
                if k + 3 < end
                    && bytes[k] == b'p'
                    && bytes[k + 1] == b'o'
                    && bytes[k + 2] == b'p'
                    && bytes[k + 3] == b'$'
                {
                    return Some((k + 4, FrameSegmentKind::Transition));
                }
            }
        }
    }

    // Stack push: push$
    //
    // The trailing `$` is its own boundary (not a valid Rust/Python/Go
    // identifier char), but a JavaScript/TypeScript identifier of the
    // form `mypush$` would otherwise match `push$` as a suffix and
    // produce a spurious StackPush region. Require a leading word
    // boundary so the scanner only matches a standalone `push$`.
    if b == b'p'
        && pos + 4 < end
        && bytes[pos + 1] == b'u'
        && bytes[pos + 2] == b's'
        && bytes[pos + 3] == b'h'
        && bytes[pos + 4] == b'$'
    {
        let leading_boundary =
            pos == 0 || (!bytes[pos - 1].is_ascii_alphanumeric() && bytes[pos - 1] != b'_');
        if leading_boundary {
            return Some((pos + 5, FrameSegmentKind::StackPush));
        }
    }

    // Stack pop (standalone): pop$
    //
    // Same rationale as `push$`: require a leading word boundary so
    // a JS-style identifier ending in `pop$` doesn't get misclassified.
    if b == b'p'
        && pos + 3 < end
        && bytes[pos + 1] == b'o'
        && bytes[pos + 2] == b'p'
        && bytes[pos + 3] == b'$'
    {
        let leading_boundary =
            pos == 0 || (!bytes[pos - 1].is_ascii_alphanumeric() && bytes[pos - 1] != b'_');
        if leading_boundary {
            return Some((pos + 4, FrameSegmentKind::StackPop));
        }
    }

    // Return statement: return <expr>?
    //
    // Detected by the `return` keyword surrounded by word boundaries on
    // BOTH sides. The trailing-boundary check (next char not in
    // [A-Za-z0-9_]) prevents matching `returns` or `return_value`. The
    // LEADING-boundary check (previous char not in [A-Za-z0-9_]) is
    // equally important — without it, the byte-by-byte walker lands on
    // the `r` of `after_return` and matches `return` mid-identifier,
    // collapsing the Rust identifier into a Frame return statement.
    // Real-world casualty: the `output_block_parser.frs` source has
    // 15+ references to a local `after_return` flag, all of which got
    // shredded by this misclassification on regen, forcing the
    // `.gen.rs` to be hand-edited.
    //
    // Closures are already skipped by `skip_nested_scope()`, so this
    // is at handler scope. Position 0 is treated as a valid leading
    // boundary (start-of-buffer counts).
    if b == b'r'
        && pos + 5 < end
        && bytes[pos + 1] == b'e'
        && bytes[pos + 2] == b't'
        && bytes[pos + 3] == b'u'
        && bytes[pos + 4] == b'r'
        && bytes[pos + 5] == b'n'
    {
        let leading_boundary =
            pos == 0 || (!bytes[pos - 1].is_ascii_alphanumeric() && bytes[pos - 1] != b'_');
        let after = pos + 6;
        let trailing_boundary =
            after >= end || (!bytes[after].is_ascii_alphanumeric() && bytes[after] != b'_');
        if leading_boundary && trailing_boundary {
            return Some((after, FrameSegmentKind::ReturnStatement));
        }
    }

    None
}

// ============================================================================
// Whitespace helper
// ============================================================================

/// Skip horizontal whitespace (spaces and tabs). Returns new position.
#[inline]
pub fn skip_ws(bytes: &[u8], mut pos: usize, end: usize) -> usize {
    while pos < end && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
        pos += 1;
    }
    pos
}

/// Skip whitespace *including* newlines (space, tab, CR, LF). Returns new
/// position. Used where a Frame construct may legitimately span lines —
/// e.g. a transition written `->` <newline> `$State` — so parsing stays
/// whitespace-invariant. It still only consumes contiguous whitespace, so
/// a `->` with no `$` target on the following line is left un-matched
/// (and falls through to native text) exactly as a same-line `-> foo`
/// would. FRAMEC_BUGS #43.
#[inline]
pub fn skip_ws_nl(bytes: &[u8], mut pos: usize, end: usize) -> usize {
    while pos < end
        && (bytes[pos] == b' ' || bytes[pos] == b'\t' || bytes[pos] == b'\n' || bytes[pos] == b'\r')
    {
        pos += 1;
    }
    pos
}

// ============================================================================
// Common helper implementations for C-like languages
// ============================================================================

/// Skip C-style line comment: // ...
pub fn skip_line_comment(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    if i + 1 < end && bytes[i] == b'/' && bytes[i + 1] == b'/' {
        let mut j = i + 2;
        while j < end && bytes[j] != b'\n' {
            j += 1;
        }
        Some(j)
    } else {
        None
    }
}

/// Skip Python/Ruby/Erlang-style line comment: `# ...` to end of line.
/// Used by the Frame-structural skipper for the GraphViz pipeline,
/// which accepts source written for any target.
pub fn skip_hash_line_comment(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    if i < end && bytes[i] == b'#' {
        let mut j = i + 1;
        while j < end && bytes[j] != b'\n' {
            j += 1;
        }
        Some(j)
    } else {
        None
    }
}

/// Skip C-style block comment: /* ... */
pub fn skip_block_comment(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    if i + 1 < end && bytes[i] == b'/' && bytes[i + 1] == b'*' {
        let mut j = i + 2;
        while j + 1 < end {
            if bytes[j] == b'*' && bytes[j + 1] == b'/' {
                return Some(j + 2);
            }
            j += 1;
        }
        Some(end) // Unterminated, consume rest
    } else {
        None
    }
}

/// Skip Python-style comment: # ...
pub fn skip_hash_comment(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    if bytes[i] == b'#' {
        let mut j = i + 1;
        while j < end && bytes[j] != b'\n' {
            j += 1;
        }
        Some(j)
    } else {
        None
    }
}

/// Skip simple string: 'x' or "x" with backslash escapes.
///
/// Handles single-quoted strings (Python, C char literals) and double-quoted
/// strings. For Rust, use `skip_rust_string` instead — this function does not
/// distinguish Rust lifetimes from char literals.
pub fn skip_simple_string(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    let b = bytes[i];
    if b == b'\'' || b == b'"' {
        let q = b;
        let mut j = i + 1;
        while j < end {
            if bytes[j] == b'\\' {
                j += 2;
                continue;
            }
            if bytes[j] == q {
                return Some(j + 1);
            }
            j += 1;
        }
        Some(end) // Unterminated
    } else {
        None
    }
}

/// Skip a Rust string or char literal, distinguishing from lifetime annotations.
///
/// Rust uses `'` for both char literals (`'a'`, `'\n'`) and lifetime annotations
/// (`'a`, `'static`, `'_`). This function only consumes `'` as a char literal
/// if the pattern matches `'X'` (single char) or `'\...'` (escape sequence).
/// Lifetimes return `None` so the scanner keeps going.
///
/// Double-quoted strings (`"..."`) are handled normally.
pub fn skip_rust_string(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    let b = bytes[i];
    if b == b'"' {
        // Double-quoted string — same as skip_simple_string
        let mut j = i + 1;
        while j < end {
            if bytes[j] == b'\\' {
                j += 2;
                continue;
            }
            if bytes[j] == b'"' {
                return Some(j + 1);
            }
            j += 1;
        }
        Some(end) // Unterminated
    } else if b == b'\'' {
        // Single quote: distinguish char literal from lifetime.
        let j = i + 1;
        if j >= end {
            return None;
        }
        if bytes[j] == b'\\' {
            // Escape sequence: '\n', '\t', '\x41', '\u{...}', etc.
            let mut k = j + 1;
            while k < end && k < j + 12 && bytes[k] != b'\'' {
                k += 1;
            }
            if k < end && bytes[k] == b'\'' {
                return Some(k + 1);
            }
            None // No closing quote — not a char literal
        } else if j + 1 < end && bytes[j + 1] == b'\'' {
            // Simple char literal: 'a', '1', '>', etc.
            Some(j + 2)
        } else {
            // Not a char literal — lifetime annotation ('a, 'static, '_)
            None
        }
    } else {
        None
    }
}

/// Skip Python triple-quoted string: '''x''' or """x"""
pub fn skip_triple_string(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    let b = bytes[i];
    if (b == b'\'' || b == b'"') && i + 2 < end && bytes[i + 1] == b && bytes[i + 2] == b {
        let q = b;
        let mut j = i + 3;
        while j + 2 < end {
            if bytes[j] == q && bytes[j + 1] == q && bytes[j + 2] == q {
                return Some(j + 3);
            }
            j += 1;
        }
        Some(end) // Unterminated
    } else {
        None
    }
}

/// Skip TypeScript/JS template literal: `...${...}...`
pub fn skip_template_literal(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    if bytes[i] == b'`' {
        let mut j = i + 1;
        let mut brace_depth = 0i32;
        while j < end {
            if bytes[j] == b'`' && brace_depth == 0 {
                return Some(j + 1);
            }
            if bytes[j] == b'\\' {
                j += 2;
                continue;
            }
            if bytes[j] == b'$' && j + 1 < end && bytes[j + 1] == b'{' {
                brace_depth += 1;
                j += 2;
                continue;
            }
            if bytes[j] == b'}' && brace_depth > 0 {
                brace_depth -= 1;
            }
            j += 1;
        }
        Some(end) // Unterminated
    } else {
        None
    }
}

/// Skip Rust raw string: r#"..."#
pub fn skip_rust_raw_string(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    if bytes[i] != b'r' {
        return None;
    }
    let mut j = i + 1;
    let mut hashes = 0usize;

    // Count leading #s
    while j < end && bytes[j] == b'#' {
        hashes += 1;
        j += 1;
    }

    // Must have opening "
    if j >= end || bytes[j] != b'"' {
        return None;
    }
    j += 1;

    // Find closing "###
    while j < end {
        if bytes[j] == b'"' {
            let mut k = j + 1;
            let mut matched = 0usize;
            while matched < hashes && k < end && bytes[k] == b'#' {
                matched += 1;
                k += 1;
            }
            if matched == hashes {
                return Some(k);
            }
        }
        j += 1;
    }
    Some(end) // Unterminated
}

/// Find end of Frame statement line for C-like languages
pub fn find_line_end_c_like(bytes: &[u8], mut j: usize, end: usize) -> usize {
    let mut in_string: Option<u8> = None;

    while j < end {
        let b = bytes[j];

        if b == b'\n' {
            break;
        }

        if let Some(q) = in_string {
            if b == b'\\' {
                j += 2;
                continue;
            }
            if b == q {
                in_string = None;
            }
            j += 1;
            continue;
        }

        if b == b';' || b == b'}' {
            break;
        }
        if b == b'/' && j + 1 < end && (bytes[j + 1] == b'/' || bytes[j + 1] == b'*') {
            break;
        }
        if b == b'\'' || b == b'"' {
            in_string = Some(b);
        }
        j += 1;
    }
    j
}

/// Find end of Frame statement line for Python
pub fn find_line_end_python(bytes: &[u8], mut j: usize, end: usize) -> usize {
    let mut in_string: Option<u8> = None;

    while j < end {
        let b = bytes[j];

        if b == b'\n' {
            break;
        }

        if let Some(q) = in_string {
            if b == b'\\' {
                j += 2;
                continue;
            }
            if b == q {
                in_string = None;
            }
            j += 1;
            continue;
        }

        if b == b'#' || b == b';' {
            break;
        }
        if b == b'\'' || b == b'"' {
            in_string = Some(b);
        }
        j += 1;
    }
    j
}

/// Find balanced paren end for C-like languages
pub fn balanced_paren_end_c_like(bytes: &[u8], mut i: usize, end: usize) -> Option<usize> {
    if i >= end || bytes[i] != b'(' {
        return None;
    }

    let mut depth = 0i32;
    let mut in_string: Option<u8> = None;

    while i < end {
        let b = bytes[i];

        if let Some(q) = in_string {
            if b == b'\\' {
                i += 2;
                continue;
            }
            if b == q {
                in_string = None;
            }
            i += 1;
            continue;
        }

        match b {
            b'\'' | b'"' => {
                in_string = Some(b);
                i += 1;
            }
            b'(' => {
                depth += 1;
                i += 1;
            }
            b')' => {
                depth -= 1;
                i += 1;
                if depth == 0 {
                    return Some(i);
                }
            }
            _ => {
                i += 1;
            }
        }
    }
    None
}

/// Skip PHP heredoc/nowdoc: <<<EOT ... EOT; or <<<'EOT' ... EOT;
/// Position `i` must be at the first `<` of `<<<`.
/// Returns Some(position after closing identifier + optional semicolon) or None if not a heredoc.
pub fn skip_php_heredoc(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    // Must start with <<<
    if i + 2 >= end || bytes[i] != b'<' || bytes[i + 1] != b'<' || bytes[i + 2] != b'<' {
        return None;
    }
    let mut j = i + 3;

    // Skip optional whitespace after <<<
    while j < end && bytes[j] == b' ' {
        j += 1;
    }

    // Determine if nowdoc (<<<'ID') or heredoc (<<<ID or <<<"ID")
    let is_quoted = j < end && (bytes[j] == b'\'' || bytes[j] == b'"');
    let quote_char = if is_quoted { bytes[j] } else { 0 };
    if is_quoted {
        j += 1; // skip opening quote
    }

    // Extract the identifier
    let id_start = j;
    while j < end && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
        j += 1;
    }
    if j == id_start {
        return None; // No identifier
    }
    let identifier = &bytes[id_start..j];

    // Skip closing quote if present
    if is_quoted {
        if j >= end || bytes[j] != quote_char {
            return None; // Mismatched quote
        }
        j += 1;
    }

    // Must be followed by newline (possibly with trailing whitespace/semicolon)
    while j < end && bytes[j] != b'\n' {
        j += 1;
    }
    if j < end {
        j += 1; // skip the newline
    }

    // Scan for the closing identifier at the start of a line
    while j < end {
        // Check if this line starts with (optional whitespace +) the identifier
        let line_start = j;
        // Skip leading whitespace (PHP 7.3+ flexible heredoc)
        while j < end && (bytes[j] == b' ' || bytes[j] == b'\t') {
            j += 1;
        }

        // Check if identifier matches
        let remaining = end - j;
        if remaining >= identifier.len() && &bytes[j..j + identifier.len()] == identifier {
            let after_id = j + identifier.len();
            // Must be followed by ; or newline or EOF
            if after_id >= end || bytes[after_id] == b'\n' || bytes[after_id] == b';' {
                // Found the end — skip past the identifier + optional semicolon + newline
                let mut result = after_id;
                if result < end && bytes[result] == b';' {
                    result += 1;
                }
                if result < end && bytes[result] == b'\n' {
                    result += 1;
                }
                return Some(result);
            }
        }

        // Skip to next line
        while j < end && bytes[j] != b'\n' {
            j += 1;
        }
        if j < end {
            j += 1; // skip newline
        }
    }
    Some(end) // Unterminated heredoc — consume to end
}

/// Skip Ruby percent literal: %Q{...}, %q[...], %w(...), %i<...>, %r|...|, etc.
/// Position `i` must be at the `%` character.
/// Returns Some(position after closing delimiter) or None if not a percent literal.
pub fn skip_ruby_percent_literal(bytes: &[u8], i: usize, end: usize) -> Option<usize> {
    if i >= end || bytes[i] != b'%' {
        return None;
    }

    let mut j = i + 1;
    if j >= end {
        return None;
    }

    // Optional letter qualifier: Q, q, w, W, i, I, s, x, r
    let next = bytes[j];
    if next.is_ascii_alphabetic() {
        // Must be a recognized qualifier
        if !matches!(
            next,
            b'Q' | b'q' | b'w' | b'W' | b'i' | b'I' | b's' | b'x' | b'r'
        ) {
            return None;
        }
        j += 1;
        if j >= end {
            return None;
        }
    }

    // Determine the delimiter
    let open = bytes[j];
    let (close, is_paired) = match open {
        b'{' => (b'}', true),
        b'[' => (b']', true),
        b'(' => (b')', true),
        b'<' => (b'>', true),
        // Any non-alphanumeric, non-space character can be a delimiter
        c if !c.is_ascii_alphanumeric() && c != b' ' && c != b'\n' => (c, false),
        _ => return None,
    };
    j += 1; // skip opening delimiter

    if is_paired {
        // Paired delimiters support nesting
        let mut depth = 1i32;
        while j < end && depth > 0 {
            if bytes[j] == b'\\' {
                j += 2; // skip escaped character
                continue;
            }
            if bytes[j] == open {
                depth += 1;
            } else if bytes[j] == close {
                depth -= 1;
            }
            j += 1;
        }
        if depth == 0 {
            Some(j)
        } else {
            Some(end) // Unterminated
        }
    } else {
        // Non-paired: scan to matching delimiter
        while j < end {
            if bytes[j] == b'\\' {
                j += 2;
                continue;
            }
            if bytes[j] == close {
                return Some(j + 1);
            }
            j += 1;
        }
        Some(end) // Unterminated
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::frame_c::compiler::native_region_scanner::rust::RustSkipper;

    /// Helper: scan a Rust handler body and return the kinds of all
    /// FrameSegment regions found, in order. The body is provided as a
    /// fully-formed string starting with `{` so the scanner can find
    /// the matching close brace via the body closer.
    fn scan_for_kinds(body: &str) -> Vec<FrameSegmentKind> {
        let bytes = body.as_bytes();
        let result = scan_native_regions(&RustSkipper, bytes, 0).expect("scan should succeed");
        result
            .regions
            .iter()
            .filter_map(|r| match r {
                Region::FrameSegment { kind, .. } => Some(*kind),
                Region::NativeText { .. } => None,
            })
            .collect()
    }

    /// Regression test for the `after_return` shred bug.
    ///
    /// Before the fix, `match_frame_statement` only checked the
    /// trailing word boundary after the `return` keyword. The
    /// byte-by-byte walker would land on the `r` of `after_return`
    /// inside `let after_return = false;` and match it as a Frame
    /// `return` statement, mangling the identifier. This was the
    /// reason `output_block_parser.gen.rs` had to be hand-edited
    /// after every regen — the .frs source's `after_return` flag
    /// (15+ references) got shredded.
    #[test]
    fn test_return_keyword_requires_leading_word_boundary() {
        // Identifier ending in `return` — must NOT be matched as a
        // Frame return statement.
        let body = "{ let after_return = false; }";
        let kinds = scan_for_kinds(body);
        assert!(
            !kinds.contains(&FrameSegmentKind::ReturnStatement),
            "`after_return = false` must not be classified as ReturnStatement, got: {:?}",
            kinds
        );
    }

    #[test]
    fn test_return_keyword_other_identifier_suffixes() {
        // Various other identifier shapes that contain the substring
        // `return` and would have been false-positively matched.
        let cases = [
            "{ let no_return = 1; }",
            "{ self.after_return = true; }",
            "{ if my_return { } }",
            "{ let _return = 0; }",
        ];
        for body in cases {
            let kinds = scan_for_kinds(body);
            assert!(
                !kinds.contains(&FrameSegmentKind::ReturnStatement),
                "Body {:?} must not produce a ReturnStatement, got: {:?}",
                body,
                kinds
            );
        }
    }

    #[test]
    fn nested_fn_returns_are_not_classified_as_frame_returns() {
        // Regression for bug #28 (RFC-0035 round 3 dogfood). A
        // nested Rust `fn name(args) -> Ret { ... return EXPR; ... }`
        // inside a handler body must be skipped as a nested scope.
        // Before the fix, `return EXPR;` inside the inner fn was
        // misclassified as a Frame ReturnStatement, emitting W415
        // and risking a splice rewrite of the wrong return.
        let body = r#"{
            fn inner(x: i32) -> i32 {
                if x < 0 { return -1; }
                return x + 1;
            }
            let r = inner(5);
        }"#;
        let kinds = scan_for_kinds(body);
        assert!(
            !kinds.contains(&FrameSegmentKind::ReturnStatement),
            "Returns inside a nested fn must be skipped, got: {:?}",
            kinds
        );
    }

    #[test]
    fn nested_fn_with_generics_and_where_skipped() {
        // Generic + where clause + complex return type — the scanner
        // must still skip the body and not classify its inner
        // `return`.
        let body = r#"{
            fn outer<T: Clone>(x: T) -> Option<T> where T: Default {
                return Some(x);
            }
            let _ = outer(0);
        }"#;
        let kinds = scan_for_kinds(body);
        assert!(
            !kinds.contains(&FrameSegmentKind::ReturnStatement),
            "Returns inside a nested generic fn must be skipped, got: {:?}",
            kinds
        );
    }

    #[test]
    fn top_level_return_still_matches_alongside_nested_fn() {
        // The fix must not over-skip — a `return` at handler scope,
        // alongside a nested fn, must still be classified as a Frame
        // ReturnStatement.
        let body = r#"{
            fn helper() -> i32 { return 1; }
            return helper();
        }"#;
        let kinds = scan_for_kinds(body);
        assert!(
            kinds.contains(&FrameSegmentKind::ReturnStatement),
            "Top-level return must still match, got: {:?}",
            kinds
        );
    }

    #[test]
    fn fn_identifier_suffix_not_misclassified() {
        // `function`, `fn_helper`, etc. must NOT be matched as the
        // start of a Rust `fn` declaration — the leading word-
        // boundary check on `fn` keyword should prevent this.
        let body = r#"{
            let function = 5;
            let fn_helper = 10;
            return function + fn_helper;
        }"#;
        let kinds = scan_for_kinds(body);
        // The top-level return must still match (no false-skip).
        assert!(
            kinds.contains(&FrameSegmentKind::ReturnStatement),
            "Top-level return after `fn`-prefixed identifiers must match, got: {:?}",
            kinds
        );
    }

    #[test]
    fn test_return_keyword_still_matches_at_word_boundary() {
        // The fix must not break legitimate `return` matches. A bare
        // `return` after whitespace (the common case) and `return`
        // at the start of a line should still match.
        let cases = [
            "{ return; }",
            "{ return 42; }",
            "{ \n    return self.value;\n }",
        ];
        for body in cases {
            let kinds = scan_for_kinds(body);
            assert!(
                kinds.contains(&FrameSegmentKind::ReturnStatement),
                "Body {:?} should produce a ReturnStatement, got: {:?}",
                body,
                kinds
            );
        }
    }

    /// Same regression but for `push$` and `pop$`. JavaScript-style
    /// identifiers can contain `$`, so a name like `mypush$` would
    /// have collided with the `push$` keyword. Less likely to bite
    /// in practice (Rust doesn't allow `$` in identifiers) but worth
    /// guarding for backends that do.
    #[test]
    fn test_push_pop_require_leading_word_boundary() {
        let body = "{ let mypush$ = 1; let mypop$ = 2; }";
        // Note: this is invalid Rust but valid JS — and the scanner
        // shouldn't classify these as Frame stack-ops regardless of
        // target language. The scanner is language-agnostic about
        // identifier shape; the leading-boundary check makes the
        // detection conservative.
        let kinds = scan_for_kinds(body);
        assert!(
            !kinds.contains(&FrameSegmentKind::StackPush),
            "`mypush$` must not be classified as StackPush, got: {:?}",
            kinds
        );
        assert!(
            !kinds.contains(&FrameSegmentKind::StackPop),
            "`mypop$` must not be classified as StackPop, got: {:?}",
            kinds
        );
    }

    #[test]
    fn test_push_pop_still_match_at_word_boundary() {
        let body = "{ push$\n pop$ }";
        let kinds = scan_for_kinds(body);
        assert!(
            kinds.contains(&FrameSegmentKind::StackPush),
            "bare `push$` should still match, got: {:?}",
            kinds
        );
        assert!(
            kinds.contains(&FrameSegmentKind::StackPop),
            "bare `pop$` should still match, got: {:?}",
            kinds
        );
    }

    #[test]
    fn test_self_call_recognized() {
        let kinds = scan_for_kinds("{ @@:self.reading() }");
        assert_eq!(kinds, vec![FrameSegmentKind::ContextSelfCall]);
    }

    #[test]
    fn test_self_call_with_args() {
        let kinds = scan_for_kinds("{ @@:self.process(a, b) }");
        assert_eq!(kinds, vec![FrameSegmentKind::ContextSelfCall]);
    }

    #[test]
    fn test_self_call_in_assignment() {
        let kinds = scan_for_kinds("{ let x = @@:self.getStatus(); }");
        assert_eq!(kinds, vec![FrameSegmentKind::ContextSelfCall]);
    }

    #[test]
    fn test_bare_self_recognized() {
        let kinds = scan_for_kinds("{ let s = @@:self; }");
        assert_eq!(kinds, vec![FrameSegmentKind::ContextSelf]);
    }

    #[test]
    fn test_self_call_with_nested_parens() {
        let kinds = scan_for_kinds("{ @@:self.process(foo(1, 2), bar()) }");
        assert_eq!(kinds, vec![FrameSegmentKind::ContextSelfCall]);
    }

    #[test]
    fn test_system_state_name_recognized() {
        // RFC-0045: the current-state name accessor is `@@:system.state.name`.
        let kinds = scan_for_kinds("{ let s = @@:system.state.name; }");
        assert_eq!(kinds, vec![FrameSegmentKind::ContextSystemState]);
    }

    #[test]
    fn test_bare_system_state_reserved() {
        // RFC-0045: bare `@@:system.state` (no `.name`) is reserved → E608.
        let kinds = scan_for_kinds("{ let s = @@:system.state; }");
        assert_eq!(kinds, vec![FrameSegmentKind::ContextSystemStateReserved]);
    }

    #[test]
    fn test_system_state_dot_other_reserved() {
        // `.state.<other>` is not the `.name` accessor → reserved → E608.
        let kinds = scan_for_kinds("{ let s = @@:system.state.foo; }");
        assert_eq!(kinds, vec![FrameSegmentKind::ContextSystemStateReserved]);
    }

    #[test]
    fn test_system_state_in_return_expr() {
        let kinds = scan_for_kinds("{ @@:(@@:system.state.name) }");
        assert_eq!(kinds, vec![FrameSegmentKind::ContextReturnExpr]);
    }

    #[test]
    fn test_system_state_word_boundary() {
        // @@:system.stateX should NOT match — 'stateX' is not 'state'
        let kinds = scan_for_kinds("{ let s = @@:system.stateX; }");
        assert!(
            !kinds.contains(&FrameSegmentKind::ContextSystemState),
            "stateX should not match @@:system.state, got: {:?}",
            kinds
        );
    }

    #[test]
    fn test_system_unknown_property() {
        // @@:system.foo should not produce a match
        let kinds = scan_for_kinds("{ let s = @@:system.foo; }");
        assert!(
            !kinds.contains(&FrameSegmentKind::ContextSystemState),
            "@@:system.foo should not match, got: {:?}",
            kinds
        );
    }

    // ===== SegmentMetadata extraction tests =====

    /// Helper: scan a body and return metadata for all FrameSegments
    fn scan_for_metadata(body: &str) -> Vec<(FrameSegmentKind, SegmentMetadata)> {
        let bytes = body.as_bytes();
        let result = scan_native_regions(&RustSkipper, bytes, 0).expect("scan should succeed");
        result
            .regions
            .iter()
            .filter_map(|r| match r {
                Region::FrameSegment { kind, metadata, .. } => Some((*kind, metadata.clone())),
                Region::NativeText { .. } => None,
            })
            .collect()
    }

    #[test]
    fn test_metadata_context_params() {
        let metas = scan_for_metadata("{ let x = @@:params.age; }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::ContextParams { key } => assert_eq!(key, "age"),
            other => panic!("Expected ContextParams, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_context_data() {
        let metas = scan_for_metadata("{ let x = @@:data.msg; }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::ContextData { key, assign_expr } => {
                assert_eq!(key, "msg");
                assert!(assign_expr.is_none());
            }
            other => panic!("Expected ContextData, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_state_var() {
        let metas = scan_for_metadata("{ $.count }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::StateVar { name, .. } => assert_eq!(name, "count"),
            other => panic!("Expected StateVar, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_self_call() {
        let metas = scan_for_metadata("{ @@:self.reading() }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::SelfCall { method, args } => {
                assert_eq!(method, "reading");
                assert_eq!(args, "()");
            }
            other => panic!("Expected SelfCall, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_self_call_with_args() {
        let metas = scan_for_metadata("{ @@:self.process(a, b) }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::SelfCall { method, args } => {
                assert_eq!(method, "process");
                assert_eq!(args, "(a, b)");
            }
            other => panic!("Expected SelfCall, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_transition_simple() {
        let metas = scan_for_metadata("{ -> $Active }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::Transition {
                target_state,
                is_pop,
                ..
            } => {
                assert_eq!(target_state, "Active");
                assert!(!is_pop);
            }
            other => panic!("Expected Transition, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_transition_pop() {
        let metas = scan_for_metadata("{ -> pop$ }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::Transition { is_pop, .. } => assert!(is_pop),
            other => panic!("Expected Transition with is_pop, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_transition_with_enter_args() {
        let metas = scan_for_metadata("{ -> (\"hello\") $Dialog }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::Transition {
                target_state,
                enter_args,
                ..
            } => {
                assert_eq!(target_state, "Dialog");
                assert!(enter_args.is_some());
            }
            other => panic!("Expected Transition, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_return_expr() {
        let metas = scan_for_metadata("{ @@:(42) }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::ReturnExpr { expr } => assert_eq!(expr, "42"),
            other => panic!("Expected ReturnExpr, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_system_instantiation() {
        use crate::frame_c::compiler::frame_ast::InstantiationKind;
        let metas = scan_for_metadata("{ let x = @@Counter(10); }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::SystemInstantiation {
                system_name,
                args,
                kind,
            } => {
                assert_eq!(system_name, "Counter");
                assert_eq!(args, "(10)");
                assert_eq!(*kind, InstantiationKind::Factory);
            }
            other => panic!("Expected SystemInstantiation, got {:?}", other),
        }
    }

    #[test]
    fn test_metadata_system_instantiation_no_init() {
        // RFC-0015 D7: `@@!Foo()` is a NoInitialization variant
        use crate::frame_c::compiler::frame_ast::InstantiationKind;
        let metas = scan_for_metadata("{ let x = @@!Counter(); }");
        assert_eq!(metas.len(), 1);
        match &metas[0].1 {
            SegmentMetadata::SystemInstantiation {
                system_name,
                args,
                kind,
            } => {
                assert_eq!(system_name, "Counter");
                assert_eq!(args, "()");
                assert_eq!(*kind, InstantiationKind::NoInitialization);
            }
            other => panic!("Expected SystemInstantiation, got {:?}", other),
        }
    }
}
