use regex::Regex;
use std::borrow::Cow;
use std::rc::Rc;
use std::sync::LazyLock;
use unicode_width::UnicodeWidthChar;

use crate::config::Config;
use crate::theme::Theme;
use crate::formatting::{LineFormatting, has_markup_bytes, parse_formatting};
use crate::types::{LINES_PER_PAGE, LineType, PAGE_WIDTH, get_marker_color};

static SCENE_NUM_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"^(.*?)\s*#([^#]+)#\s*$").unwrap());








#[derive(Clone)]
pub struct VisualRow {
    
    
    pub line_idx: usize,

    
    pub char_start: usize,

    
    pub char_end: usize,

    
    
    pub raw_text: String,

    
    pub line_type: LineType,

    
    
    pub indent: u16,

    
    
    
    pub is_active: bool,

    
    
    pub scene_num: Option<String>,

    
    
    pub page_num: Option<usize>,

    
    
    pub override_color: Option<ratatui::style::Color>,

    
    
    pub fmt: Rc<LineFormatting>,

    
    
    
    
    pub is_phantom: bool,
}

impl VisualRow {
    
    
    
    
    
    
    
    pub fn logical_to_visual_x(&self, logical_x: usize) -> u16 {
        if logical_x <= self.char_start {
            return self.indent;
        }
        let mut vis = self.indent;
        for (i, c) in self.raw_text.chars().enumerate() {
            let global_i = self.char_start + i;
            if global_i >= logical_x {
                break;
            }
            if self.is_active || !self.fmt.hidden_chars.contains(&global_i) {
                vis += c.width().unwrap_or(0) as u16;
            }
        }
        vis
    }

    
    
    
    
    
    
    pub fn visual_to_logical_x(&self, vis_x: u16, is_last_in_logical: bool) -> usize {
        if vis_x <= self.indent {
            return self.char_start;
        }
        let mut current_vis = self.indent;
        let max_logical = if is_last_in_logical {
            self.char_end
        } else {
            self.char_end.saturating_sub(1)
        };

        for (i, c) in self.raw_text.chars().enumerate() {
            let log_x = self.char_start + i;
            if log_x >= max_logical {
                break;
            }
            let w = if self.is_active || !self.fmt.hidden_chars.contains(&log_x) {
                c.width().unwrap_or(0) as u16
            } else {
                0
            };
            if current_vis + w > vis_x {
                return log_x;
            }
            current_vis += w;
        }
        max_logical
    }
}


















pub fn strip_sigils(raw: &str, lt: LineType) -> &str {
    let trimmed = raw.trim_start();
    match lt {
        LineType::Lyrics if trimmed.starts_with('~') => trimmed[1..].trim_start(),
        LineType::Action | LineType::Shot if trimmed.starts_with("!!") => &trimmed[2..],
        LineType::Action | LineType::Shot if trimmed.starts_with('!') => &trimmed[1..],
        LineType::SceneHeading if trimmed.starts_with('.') && !trimmed.starts_with("..") => {
            &trimmed[1..]
        }
        LineType::Transition if trimmed.starts_with('>') => trimmed[1..].trim_start(),
        LineType::Centered if trimmed.starts_with('>') && trimmed.ends_with('<') => {
            trimmed[1..trimmed.len() - 1].trim()
        }
        LineType::Character | LineType::DualDialogueCharacter if trimmed.starts_with('@') => {
            trimmed[1..].trim_end_matches('^').trim()
        }
        LineType::Character | LineType::DualDialogueCharacter => raw.trim_end_matches('^').trim(),
        LineType::Synopsis if trimmed.starts_with('=') => trimmed[1..].trim_start(),
        LineType::Section if trimmed.starts_with('#') => trimmed[1..].trim_start(),
        LineType::MetadataTitle => {
            if let Some(idx) = raw.find(':') {
                raw[idx + 1..].trim_start()
            } else {
                raw
            }
        }
        _ => raw,
    }
}






pub fn sigil_left_chars(raw: &str, lt: LineType) -> usize {
    let stripped = strip_sigils(raw, lt);
    if stripped.as_ptr() >= raw.as_ptr() {
        let byte_offset = stripped.as_ptr() as usize - raw.as_ptr() as usize;
        raw[..byte_offset].chars().count()
    } else {
        0
    }
}






pub fn is_printable(lt: LineType) -> bool {
    !matches!(
        lt,
        LineType::MetadataTitle
            | LineType::MetadataKey
            | LineType::MetadataValue
            | LineType::Boneyard
            | LineType::Note
            | LineType::PageBreak
    )
}

fn token_metrics(text: &str, is_active: bool, hide_markup: bool) -> (u16, u16, bool) {
    let mut width = 0;
    let mut trailing_spaces = 0;
    let mut is_pure = true;

    for c in text.chars() {
        if !is_active && hide_markup && (c == '*' || c == '_') {
            continue;
        }
        let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16;
        width += w;

        if c.is_whitespace() {
            trailing_spaces += w;
        } else {
            trailing_spaces = 0;
            is_pure = false;
        }
    }

    let trimmed_width = if is_pure {
        width
    } else {
        width.saturating_sub(trailing_spaces)
    };
    (trimmed_width, width, is_pure)
}

fn calculate_indent(
    lt: LineType,
    text: &str,
    base_indent: u16,
    is_active: bool,
    hide_markup: bool,
) -> u16 {
    match lt {
        LineType::Centered | LineType::Lyrics => {
            let w = token_metrics(text, is_active, hide_markup).1;
            PAGE_WIDTH.saturating_sub(w) / 2
        }
        LineType::Transition => {
            let w = token_metrics(text, is_active, hide_markup).1;
            PAGE_WIDTH.saturating_sub(w)
        }
        _ => base_indent,
    }
}

struct TokenizeText<'a> {
    text: &'a str,
    pos: usize,
    prev_was_sep: bool,
    done: bool,
}

impl<'a> TokenizeText<'a> {
    fn new(text: &'a str) -> Self {
        Self {
            text,
            pos: 0,
            prev_was_sep: true,
            done: false,
        }
    }
}

impl<'a> Iterator for TokenizeText<'a> {
    type Item = &'a str;

    fn next(&mut self) -> Option<Self::Item> {
        if self.done {
            return None;
        }
        if self.text.is_empty() {
            self.done = true;
            return Some("");
        }
        if self.pos >= self.text.len() {
            self.done = true;
            return None;
        }

        let start = self.pos;
        let mut current_pos = start;

        for c in self.text[start..].chars() {
            let is_sep = c.is_whitespace() || c == '-';
            current_pos += c.len_utf8();

            if is_sep && !self.prev_was_sep {
                self.prev_was_sep = is_sep;
                self.pos = current_pos;
                return Some(&self.text[start..current_pos]);
            }
            self.prev_was_sep = is_sep;
        }

        self.pos = current_pos;
        Some(&self.text[start..current_pos])
    }
}















pub fn build_layout(
    lines: &[String],
    types: &[LineType],
    active_line: usize,
    config: &Config,
    theme: &Theme,
) -> Vec<VisualRow> {
    let mut rows: Vec<VisualRow> = Vec::with_capacity(lines.len() + 32);
    let mut last_speaking_character = String::new();
    let mut in_dual_dialogue = false;
    let mut scene_counter = 0;
    let mut printable_row_count = 0;
    let mut page_number = 1;
    let mut page_num_pending = true;
    let mut current_line = String::with_capacity(PAGE_WIDTH as usize * 4);
    let mut active_note_color: Option<ratatui::style::Color> = None;
    let empty_fmt = Rc::new(LineFormatting::default());

    for (i, (line, &lt)) in lines.iter().zip(types.iter()).enumerate() {
        let is_active = i == active_line;
        let mut scene_num: Option<String> = None;
        let mut raw_line = Cow::Borrowed(line.as_str());
        let mut line_override_color = None;
        let format_data = if !has_markup_bytes(&raw_line) {
            empty_fmt.clone()
        } else {
            Rc::new(parse_formatting(&raw_line, theme))
        };

        if lt == LineType::Note {
            if let Some(start) = raw_line.find("[[") {
                let end_offset = raw_line[start..]
                    .find("]]")
                    .unwrap_or(raw_line.len() - start);
                let content = &raw_line[start + 2..start + end_offset];
                active_note_color = get_marker_color(content, theme);
            }
            line_override_color = active_note_color;

            if let Some(end) = raw_line.rfind("]]")
                && !raw_line[end..].contains("[[")
            {
                active_note_color = None;
            }
        } else {
            active_note_color = None;

            if matches!(
                lt,
                LineType::SceneHeading | LineType::Section | LineType::Synopsis
            ) && let Some(start) = raw_line.rfind("[[")
            {
                let end_offset = raw_line[start..]
                    .find("]]")
                    .unwrap_or(raw_line.len() - start);
                let content = &raw_line[start + 2..start + end_offset];
                line_override_color = get_marker_color(content, theme);
            }
        }

        if matches!(lt, LineType::SceneHeading | LineType::Transition) {
            last_speaking_character.clear();
        }

        match lt {
            LineType::DualDialogueCharacter => in_dual_dialogue = true,
            LineType::Character
            | LineType::SceneHeading
            | LineType::Transition
            | LineType::Action
            | LineType::Shot
            | LineType::Section
            | LineType::Synopsis
            | LineType::PageBreak
            | LineType::Centered
            | LineType::Lyrics => {
                in_dual_dialogue = false;
            }
            LineType::Empty => {
                if i > 0
                    && !matches!(
                        types[i - 1],
                        LineType::Character
                            | LineType::DualDialogueCharacter
                            | LineType::Dialogue
                            | LineType::Parenthetical
                    )
                {
                    in_dual_dialogue = false;
                }
            }
            _ => {}
        }

        if lt == LineType::SceneHeading {
            let mut explicit_scene_num = None;

            if raw_line.trim_end().ends_with('#')
                && let Some(caps) = SCENE_NUM_RE.captures(&raw_line)
            {
                let inner = caps[2].trim();
                if !inner.is_empty() {
                    explicit_scene_num = Some(inner.to_string());

                    if !is_active {
                        raw_line = Cow::Owned(caps[1].to_string());
                    }
                }
            }

            if let Some(ref s) = explicit_scene_num {
                let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
                if let Ok(n) = digits.parse::<usize>() {
                    scene_counter = n;
                }
            } else {
                scene_counter += 1;
            }

            if config.show_scene_numbers {
                scene_num = explicit_scene_num.or_else(|| Some(scene_counter.to_string()));
            }
        }

        if lt == LineType::SceneHeading && config.heading_spacing > 0 && i > 0 {
            let mut physical_empty_count = 0;
            let mut k = i;
            while k > 0 {
                k -= 1;
                if types[k] == LineType::Empty {
                    physical_empty_count += 1;
                } else {
                    break;
                }
            }

            if physical_empty_count < config.heading_spacing {
                let diff = config.heading_spacing - physical_empty_count;
                for _ in 0..diff {
                    rows.push(VisualRow {
                        line_idx: i.saturating_sub(1),
                        char_start: 0,
                        char_end: 0,
                        raw_text: String::new(),
                        line_type: LineType::Empty,
                        indent: 0,
                        is_active: false,
                        scene_num: None,
                        page_num: None,
                        override_color: None,
                        fmt: Rc::new(LineFormatting::default()),
                        is_phantom: true,
                    });
                    printable_row_count += 1;
                    if printable_row_count % LINES_PER_PAGE == 0 {
                        page_number += 1;
                        page_num_pending = true;
                    }
                }
            }
        }

        if lt == LineType::PageBreak {
            let display_text = if is_active {
                raw_line.to_string()
            } else {
                let fill_char = if config.force_ascii { "-" } else { "─" };
                fill_char.repeat(PAGE_WIDTH as usize)
            };

            rows.push(VisualRow {
                line_idx: i,
                char_start: 0,
                char_end: display_text.chars().count(),
                raw_text: display_text,
                line_type: lt,
                indent: 0,
                is_active,
                scene_num: None,
                page_num: None,
                override_color: None,
                fmt: Rc::clone(&format_data),
                is_phantom: false,
            });
            page_number += 1;
            printable_row_count = 0;
            page_num_pending = true;
            continue;
        }

        let mut fmt_rules = lt.fmt();
        if lt == LineType::Empty && i > 0 {
            match types[i - 1] {
                LineType::Character
                | LineType::DualDialogueCharacter
                | LineType::Parenthetical
                | LineType::Dialogue => {
                    fmt_rules.indent = LineType::Dialogue.fmt().indent;
                }
                _ => {}
            }
        }

        if in_dual_dialogue
            && matches!(
                lt,
                LineType::DualDialogueCharacter
                    | LineType::Dialogue
                    | LineType::Parenthetical
                    | LineType::Empty
            )
        {
            fmt_rules.indent += 10;
            if let Some(wrap_indent) = fmt_rules.wrap_indent.as_mut() {
                *wrap_indent += 10;
            }
        }

        let mut display = if is_active {
            Cow::Borrowed(raw_line.as_ref())
        } else {
            Cow::Borrowed(strip_sigils(&raw_line, lt))
        };

        if !is_active
            && matches!(
                lt,
                LineType::SceneHeading | LineType::Section | LineType::Synopsis
            )
            && display.contains("[[")
        {
            let mut cleaned = String::with_capacity(display.len());
            let mut in_note = false;
            let chars: Vec<char> = display.chars().collect();
            let mut j = 0;
            while j < chars.len() {
                if j + 1 < chars.len() && chars[j] == '[' && chars[j + 1] == '[' {
                    in_note = true;
                    j += 2;
                    continue;
                }
                if j + 1 < chars.len() && chars[j] == ']' && chars[j + 1] == ']' && in_note {
                    in_note = false;
                    j += 2;
                    continue;
                }
                if !in_note {
                    cleaned.push(chars[j]);
                }
                j += 1;
            }
            display = Cow::Owned(cleaned.trim_end().to_string());
        }

        let mut final_display = display.clone();

        if config.auto_contd && (lt == LineType::Character || lt == LineType::DualDialogueCharacter)
        {
            let clean_name = strip_sigils(&raw_line, lt).trim().to_uppercase();
            let compare_name = if let Some(idx) = clean_name.find('(') {
                clean_name[..idx].trim().to_string()
            } else {
                clean_name.clone()
            };

            if compare_name == last_speaking_character
                && !compare_name.is_empty()
                && !is_active
                && !clean_name.contains(&config.contd_extension)
            {
                final_display = Cow::Owned(format!("{} {}", display, config.contd_extension));
            }
            last_speaking_character = compare_name;
        } else if lt == LineType::Character || lt == LineType::DualDialogueCharacter {
            let clean_name = strip_sigils(&raw_line, lt).trim().to_uppercase();
            let compare_name = if let Some(idx) = clean_name.find('(') {
                clean_name[..idx].trim().to_string()
            } else {
                clean_name.clone()
            };
            last_speaking_character = compare_name;
        }

        let sigil_left = if is_active {
            0
        } else {
            sigil_left_chars(&raw_line, lt)
        };
        let total_original_chars = raw_line.chars().count();
        let mut row_disp_start: usize = 0;
        let mut cur_w = 0;
        let tokens = TokenizeText::new(&final_display);
        let mut logical_rows = Vec::new();

        current_line.clear();
        let mut current_line_char_count = 0;

        for token in tokens {
            let mut remaining_token = token;

            while !remaining_token.is_empty() {
                let (token_w_trimmed, token_w_total, token_is_pure_space) =
                    token_metrics(remaining_token, is_active, config.hide_markup);

                if !current_line.is_empty()
                    && !token_is_pure_space
                    && cur_w + token_w_trimmed > fmt_rules.width
                {
                    let raw_start = (sigil_left + row_disp_start).min(total_original_chars);
                    let raw_end = (raw_start + current_line_char_count).min(total_original_chars);
                    let current_indent = calculate_indent(
                        lt,
                        &current_line,
                        fmt_rules.indent,
                        is_active,
                        config.hide_markup,
                    );

                    logical_rows.push(VisualRow {
                        line_idx: i,
                        char_start: raw_start,
                        char_end: raw_end,
                        raw_text: current_line.clone(),
                        line_type: lt,
                        indent: current_indent,
                        is_active,
                        scene_num: scene_num.take(),
                        page_num: None,
                        override_color: line_override_color,
                        fmt: Rc::clone(&format_data),
                        is_phantom: false,
                    });

                    if let Some(wrap_indent) = fmt_rules.wrap_indent {
                        fmt_rules.indent = wrap_indent;
                    }

                    row_disp_start += current_line_char_count;
                    current_line.clear();
                    current_line_char_count = 0;
                    cur_w = 0;
                    continue;
                }

                if cur_w + token_w_trimmed > fmt_rules.width {
                    let space_left = fmt_rules.width.saturating_sub(cur_w);
                    let mut split_byte_idx = 0;
                    let mut acc_w = 0;

                    for (k, (byte_idx, c)) in remaining_token.char_indices().enumerate() {
                        let cw = if !is_active && config.hide_markup && (c == '*' || c == '_') {
                            0
                        } else {
                            unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16
                        };
                        acc_w += cw;

                        if acc_w > space_left {
                            if k == 0 && current_line.is_empty() {
                                split_byte_idx = byte_idx + c.len_utf8();
                            }
                            break;
                        }
                        split_byte_idx = byte_idx + c.len_utf8();
                    }

                    let part1 = &remaining_token[..split_byte_idx];
                    let part2 = &remaining_token[split_byte_idx..];

                    current_line.push_str(part1);
                    current_line_char_count += part1.chars().count();

                    let raw_start = (sigil_left + row_disp_start).min(total_original_chars);
                    let raw_end = (raw_start + current_line_char_count).min(total_original_chars);
                    let current_indent = calculate_indent(
                        lt,
                        &current_line,
                        fmt_rules.indent,
                        is_active,
                        config.hide_markup,
                    );

                    logical_rows.push(VisualRow {
                        line_idx: i,
                        char_start: raw_start,
                        char_end: raw_end,
                        raw_text: current_line.clone(),
                        line_type: lt,
                        indent: current_indent,
                        is_active,
                        scene_num: scene_num.take(),
                        page_num: None,
                        override_color: line_override_color,
                        fmt: Rc::clone(&format_data),
                        is_phantom: false,
                    });

                    if let Some(wrap_indent) = fmt_rules.wrap_indent {
                        fmt_rules.indent = wrap_indent;
                    }

                    row_disp_start += current_line_char_count;
                    current_line.clear();
                    current_line_char_count = 0;
                    cur_w = 0;
                    remaining_token = part2;
                } else {
                    current_line.push_str(remaining_token);
                    current_line_char_count += remaining_token.chars().count();
                    cur_w += token_w_total;
                    break;
                }
            }
        }

        let raw_start = (sigil_left + row_disp_start).min(total_original_chars);
        let raw_end = (raw_start + current_line_char_count).min(total_original_chars);
        let current_indent = calculate_indent(
            lt,
            &current_line,
            fmt_rules.indent,
            is_active,
            config.hide_markup,
        );

        logical_rows.push(VisualRow {
            line_idx: i,
            char_start: raw_start,
            char_end: raw_end,
            raw_text: current_line.clone(),
            line_type: lt,
            indent: current_indent,
            is_active,
            scene_num,
            page_num: None,
            override_color: line_override_color,
            fmt: Rc::clone(&format_data),
            is_phantom: false,
        });

        if !config.break_actions && lt == LineType::Action {
            let current_page_remaining = LINES_PER_PAGE - (printable_row_count % LINES_PER_PAGE);
            if logical_rows.len() > current_page_remaining && logical_rows.len() <= LINES_PER_PAGE {
                printable_row_count += current_page_remaining;
                page_number += 1;
                page_num_pending = true;
            }
        }

        for mut r in logical_rows {
            if is_printable(lt) {
                if page_num_pending && config.show_page_numbers && lt != LineType::Empty {
                    r.page_num = Some(page_number);
                    page_num_pending = false;
                }
                printable_row_count += 1;
                if printable_row_count > 0 && printable_row_count % LINES_PER_PAGE == 0 {
                    page_number += 1;
                    page_num_pending = true;
                }
            }
            rows.push(r);
        }
    }

    rows
}








pub fn find_visual_cursor(layout: &[VisualRow], cursor_y: usize, cursor_x: usize) -> (usize, u16) {
    let mut last_for_line = None;

    for (vi, row) in layout.iter().enumerate() {
        if row.is_phantom {
            continue;
        }
        if row.line_idx != cursor_y {
            continue;
        }
        last_for_line = Some(vi);

        let mut is_last = true;
        for next_row in layout.iter().skip(vi + 1) {
            if !next_row.is_phantom && next_row.line_idx == cursor_y {
                is_last = false;
                break;
            } else if !next_row.is_phantom {
                break;
            }
        }

        if cursor_x >= row.char_start {
            let in_range = if is_last {
                cursor_x <= row.char_end
            } else {
                cursor_x < row.char_end
            };
            if in_range {
                return (vi, row.logical_to_visual_x(cursor_x));
            }
        }
    }

    let fallback_vi = last_for_line.unwrap_or(0);
    let fallback_x = layout
        .get(fallback_vi)
        .map(|r| r.logical_to_visual_x(cursor_x))
        .unwrap_or(0);
    (fallback_vi, fallback_x)
}

#[cfg(test)]
mod layout_tests {
    use super::*;

    #[test]
    fn test_strip_sigils_scene_heading() {
        assert_eq!(strip_sigils(".HEADING", LineType::SceneHeading), "HEADING");
    }

    #[test]
    fn test_strip_sigils_action() {
        assert_eq!(strip_sigils("!ACTION", LineType::Action), "ACTION");
    }

    #[test]
    fn test_strip_sigils_shot() {
        assert_eq!(strip_sigils("!!SHOT", LineType::Shot), "SHOT");
    }

    #[test]
    fn test_strip_sigils_lyrics() {
        assert_eq!(strip_sigils("~SONG", LineType::Lyrics), "SONG");
    }

    #[test]
    fn test_strip_sigils_transition() {
        assert_eq!(strip_sigils(">FADE", LineType::Transition), "FADE");
    }

    #[test]
    fn test_strip_sigils_centered() {
        assert_eq!(strip_sigils(">CENTER<", LineType::Centered), "CENTER");
    }

    #[test]
    fn test_strip_sigils_character() {
        assert_eq!(strip_sigils("@NAME", LineType::Character), "NAME");
    }

    #[test]
    fn test_strip_sigils_dual_character() {
        assert_eq!(
            strip_sigils("@NAME^", LineType::DualDialogueCharacter),
            "NAME"
        );
        assert_eq!(
            strip_sigils("NAME^", LineType::DualDialogueCharacter),
            "NAME"
        );
    }

    #[test]
    fn test_strip_sigils_metadata() {
        assert_eq!(
            strip_sigils("Title: Value", LineType::MetadataTitle),
            "Value"
        );
        assert_eq!(
            strip_sigils("   Value", LineType::MetadataValue),
            "   Value"
        );
    }

    #[test]
    fn test_sigil_left_chars_calculation() {
        assert_eq!(sigil_left_chars(".HEADING", LineType::SceneHeading), 1);
        assert_eq!(sigil_left_chars("!!SHOT", LineType::Shot), 2);
        assert_eq!(sigil_left_chars(">CENTER<", LineType::Centered), 1);
        assert_eq!(sigil_left_chars("Title: Value", LineType::MetadataTitle), 7);
        assert_eq!(sigil_left_chars("   Value", LineType::MetadataValue), 0);
    }

    #[test]
    fn test_is_printable() {
        assert!(is_printable(LineType::Action));
        assert!(is_printable(LineType::SceneHeading));
        assert!(is_printable(LineType::Character));
        assert!(is_printable(LineType::Dialogue));
        assert!(!is_printable(LineType::Note));
        assert!(!is_printable(LineType::Boneyard));
        assert!(!is_printable(LineType::MetadataTitle));
        assert!(!is_printable(LineType::PageBreak));
    }

    #[test]
    fn test_build_layout_scene_numbering() {
        let config = Config {
            show_scene_numbers: true,
            ..Config::default()
        };

        let lines = vec![
            "INT. SCENE ONE".to_string(),
            "".to_string(),
            "EXT. SCENE TWO".to_string(),
        ];
        let types = vec![
            LineType::SceneHeading,
            LineType::Empty,
            LineType::SceneHeading,
        ];

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());

        assert_eq!(layout[0].scene_num, Some("1".to_string()));
        assert_eq!(layout[2].scene_num, Some("2".to_string()));
    }

    #[test]
    fn test_build_layout_auto_contd() {
        let config = Config {
            auto_contd: true,
            contd_extension: "(ПРОД.)".to_string(),
            ..Config::default()
        };
        let lines = vec![
            "МАТРОС".to_string(),
            "Капитан, у нас айсберг по курсу корабля!".to_string(),
            "".to_string(),
            "КАПИТАН".to_string(),
            "(удивлённо)".to_string(),
            "Айсберг по курсу корабля?".to_string(),
            "".to_string(),
            "КАПИТАН".to_string(),
            "Дороговато!".to_string(),
        ];
        let types = vec![
            LineType::Character,
            LineType::Dialogue,
            LineType::Empty,
            LineType::Character,
            LineType::Parenthetical,
            LineType::Dialogue,
            LineType::Empty,
            LineType::Character,
            LineType::Dialogue,
        ];
        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());

        assert_eq!(layout[0].raw_text, "МАТРОС");
        assert_eq!(layout[4].raw_text, "КАПИТАН");
        assert_eq!(layout[8].raw_text, "КАПИТАН (ПРОД.)");
    }

    #[test]
    fn test_build_layout_no_auto_contd_when_active() {
        let config = Config {
            auto_contd: true,
            ..Config::default()
        };
        let lines = vec![
            "CHARLOTTE".to_string(),
            "Text".to_string(),
            "".to_string(),
            "CHARLOTTE".to_string(),
        ];
        let types = vec![
            LineType::Character,
            LineType::Dialogue,
            LineType::Empty,
            LineType::Character,
        ];
        let layout = build_layout(&lines, &types, 3, &config, &Theme::default());
        assert_eq!(layout[0].raw_text, "CHARLOTTE");
        assert_eq!(layout[3].raw_text, "CHARLOTTE");
    }

    #[test]
    fn test_build_layout_phantom_lines_for_spacing() {
        let config = Config {
            heading_spacing: 3,
            ..Config::default()
        };
        let lines = vec![
            "INT. ONE".to_string(),
            "Action".to_string(),
            "INT. TWO".to_string(),
        ];
        let types = vec![
            LineType::SceneHeading,
            LineType::Action,
            LineType::SceneHeading,
        ];
        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());
        let phantoms = layout.iter().filter(|r| r.is_phantom).count();
        assert_eq!(phantoms, 3);
    }

    #[test]
    fn test_build_layout_page_break_injection() {
        let config = Config::default();
        let lines = vec!["===".to_string()];
        let types = vec![LineType::PageBreak];
        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());
        assert_eq!(layout[0].raw_text, "─".repeat(PAGE_WIDTH as usize));
    }

    #[test]
    fn test_layout_page_break_force_ascii() {
        let mut config = Config::default();
        config.force_ascii = true;
        let lines = vec!["===".to_string()];
        let types = vec![LineType::PageBreak];
        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());
        assert_eq!(layout[0].raw_text, "-".repeat(PAGE_WIDTH as usize));
    }

    #[test]
    fn test_visual_row_logical_to_visual_x() {
        let row = VisualRow {
            line_idx: 0,
            char_start: 0,
            char_end: 10,
            raw_text: "Test **bold**".to_string(),
            line_type: LineType::Action,
            indent: 5,
            is_active: false,
            scene_num: None,
            page_num: None,
            override_color: None,
            fmt: Rc::new(parse_formatting("Test **bold**", &Theme::adaptive())),
            is_phantom: false,
        };
        assert_eq!(row.logical_to_visual_x(0), 5);
        assert_eq!(row.logical_to_visual_x(5), 10);
        assert_eq!(row.logical_to_visual_x(7), 10);
    }

    #[test]
    fn test_visual_row_visual_to_logical_x() {
        let row = VisualRow {
            line_idx: 0,
            char_start: 0,
            char_end: 13,
            raw_text: "Test **bold**".to_string(),
            line_type: LineType::Action,
            indent: 5,
            is_active: false,
            scene_num: None,
            page_num: None,
            override_color: None,
            fmt: Rc::new(parse_formatting("Test **bold**", &Theme::adaptive())),
            is_phantom: false,
        };
        assert_eq!(row.visual_to_logical_x(5, true), 0);
        assert_eq!(row.visual_to_logical_x(10, true), 7);
        assert_eq!(row.visual_to_logical_x(100, true), 13);
    }

    #[test]
    fn test_layout_word_wrapping() {
        let config = Config::default();

        let long_action = "This is a very, very, very, very, very long action line that should definitely exceed the standard character limit.".to_string();

        let layout = build_layout(&[long_action][..], &[LineType::Action][..], 99, &config, &Theme::default());

        assert!(layout.len() >= 2, "Line was not wrapped correctly");

        assert_eq!(layout[0].line_idx, 0);
        assert_eq!(layout[1].line_idx, 0);

        assert_eq!(layout[0].char_start, 0);
        assert!(layout[0].char_end > 0);
        assert_eq!(layout[1].char_start, layout[0].char_end);

        let first_line_width =
            unicode_width::UnicodeWidthStr::width(layout[0].raw_text.trim_end_matches(' '));
        assert!(first_line_width <= crate::types::PAGE_WIDTH as usize);
    }

    #[test]
    fn test_layout_hardcoded_scene_numbers_stripped() {
        let config = Config::default();

        let lines = vec!["INT. KITCHEN - DAY #12A#".to_string()];
        let types = vec![LineType::SceneHeading];

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());

        assert_eq!(layout[0].raw_text, "INT. KITCHEN - DAY");
    }

    #[test]
    fn test_layout_no_break_actions() {
        let mut config = Config::default();
        config.break_actions = false;

        let mut lines = vec!["".to_string(); 54];
        let mut types = vec![LineType::Empty; 54];

        lines.push("A very long action that takes multiple visual lines on the screen because it exceeds the limit.".to_string());
        types.push(LineType::Action);

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());

        let action_rows: Vec<&VisualRow> = layout
            .iter()
            .filter(|r| r.line_type == LineType::Action)
            .collect();

        assert_eq!(action_rows[0].page_num, Some(2));
    }

    #[test]
    fn test_layout_hard_wrap_long_word() {
        let config = Config::default();

        let long_action = "A".repeat(100);

        let layout = build_layout(&[long_action][..], &[LineType::Action][..], 99, &config, &Theme::default());

        let rows: Vec<_> = layout.into_iter().filter(|r| !r.is_phantom).collect();

        assert_eq!(rows.len(), 2, "Line was not hard-wrapped correctly");

        assert_eq!(rows[0].char_start, 0);
        assert_eq!(rows[0].char_end, 60);
        assert_eq!(rows[1].char_start, 60);
        assert_eq!(rows[1].char_end, 100);

        assert_eq!(rows[0].raw_text, "A".repeat(60));
        assert_eq!(rows[1].raw_text, "A".repeat(40));
    }

    #[test]
    fn test_layout_hard_wrap_with_markup() {
        let config = Config::default();

        let long_action = format!("**{}**", "A".repeat(100));

        let layout = build_layout(&[long_action][..], &[LineType::Action][..], 99, &config, &Theme::default());

        let rows: Vec<_> = layout.into_iter().filter(|r| !r.is_phantom).collect();
        assert_eq!(rows.len(), 2);

        assert_eq!(rows[0].raw_text, format!("**{}", "A".repeat(60)));
        assert_eq!(rows[1].raw_text, format!("{}**", "A".repeat(40)));
    }

    #[test]
    fn test_layout_page_break_active_vs_inactive() {
        let config = Config::default();
        let lines = vec!["===".to_string()];
        let types = vec![LineType::PageBreak];

        let layout_inactive = build_layout(&lines, &types, 99, &config, &Theme::default());
        assert_eq!(layout_inactive[0].raw_text, "─".repeat(PAGE_WIDTH as usize));

        let layout_active = build_layout(&lines, &types, 0, &config, &Theme::default());
        assert_eq!(layout_active[0].raw_text, "===");
    }

    #[test]
    fn test_layout_show_scene_numbers_disabled() {
        let mut config = Config::default();
        config.show_scene_numbers = false;

        let lines = vec!["INT. SCENE ONE".to_string()];
        let types = vec![LineType::SceneHeading];

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());
        assert_eq!(
            layout[0].scene_num, None,
            "Scene number should be None when disabled"
        );
    }

    #[test]
    fn test_layout_show_page_numbers_disabled() {
        let mut config = Config::default();
        config.show_page_numbers = false;

        let lines = vec!["Action line".to_string()];
        let types = vec![LineType::Action];

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());

        assert_eq!(
            layout[0].page_num, None,
            "Page number should be None when disabled"
        );
    }

    #[test]
    fn test_layout_auto_contd_disabled() {
        let mut config = Config::default();
        config.auto_contd = false;

        let lines = vec![
            "CHARLOTTE".to_string(),
            "Text".to_string(),
            "".to_string(),
            "CHARLOTTE".to_string(),
        ];
        let types = vec![
            LineType::Character,
            LineType::Dialogue,
            LineType::Empty,
            LineType::Character,
        ];

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());
        assert_eq!(layout[0].raw_text, "CHARLOTTE");
        assert_eq!(
            layout[3].raw_text, "CHARLOTTE",
            "Should NOT append (CONT'D) when disabled"
        );
    }

    #[test]
    fn test_layout_break_actions_enabled() {
        let mut config = Config::default();
        config.break_actions = true;

        let mut lines = vec!["".to_string(); 54];
        let mut types = vec![LineType::Empty; 54];

        lines.push("A very long action that takes multiple visual lines on the screen because it exceeds the limit.".to_string());
        types.push(LineType::Action);

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());

        let action_rows: Vec<&VisualRow> = layout
            .iter()
            .filter(|r| r.line_type == LineType::Action)
            .collect();

        assert_eq!(
            action_rows[0].page_num,
            Some(1),
            "First line of action should remain on page 1 when breaking is allowed"
        );
    }

    #[test]
    fn test_layout_smart_heading_spacing() {
        let config = Config {
            heading_spacing: 2,
            ..Config::default()
        };

        let lines = vec![
            "Action 1".to_string(),
            "INT. SCENE 1".to_string(),
            "Action 2".to_string(),
            "".to_string(),
            "INT. SCENE 2".to_string(),
            "Action 3".to_string(),
            "".to_string(),
            "".to_string(),
            "INT. SCENE 3".to_string(),
            "Action 4".to_string(),
            "".to_string(),
            "".to_string(),
            "".to_string(),
            "INT. SCENE 4".to_string(),
        ];

        let types = vec![
            LineType::Action,
            LineType::SceneHeading,
            LineType::Action,
            LineType::Empty,
            LineType::SceneHeading,
            LineType::Action,
            LineType::Empty,
            LineType::Empty,
            LineType::SceneHeading,
            LineType::Action,
            LineType::Empty,
            LineType::Empty,
            LineType::Empty,
            LineType::SceneHeading,
        ];

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());

        let phantoms: Vec<_> = layout.iter().filter(|r| r.is_phantom).collect();

        assert_eq!(
            phantoms.len(),
            3,
            "Smart spacing failed to calculate correct phantom lines"
        );
    }

    #[test]
    fn test_layout_empty_line_preserves_spaces() {
        let config = Config::default();
        let lines = vec!["   ".to_string()];
        let types = vec![LineType::Empty];

        let layout_active = build_layout(&lines, &types, 0, &config, &Theme::default());
        assert_eq!(layout_active[0].raw_text, "   ");
        assert_eq!(layout_active[0].char_end, 3);

        let layout_inactive = build_layout(&lines, &types, 99, &config, &Theme::default());
        assert_eq!(layout_inactive[0].raw_text, "   ");
        assert_eq!(layout_inactive[0].char_end, 3);
    }

    #[test]
    fn test_layout_empty_line_exceeding_width_wraps() {
        let config = Config::default();

        let lines = vec![" ".repeat(130)];
        let types = vec![LineType::Empty];

        let layout = build_layout(&lines, &types, 0, &config, &Theme::default());

        assert_eq!(layout.len(), 3, "Empty line should be wrapped into 3 rows");

        assert_eq!(layout[0].char_start, 0);
        assert_eq!(layout[0].char_end, 60);
        assert_eq!(layout[0].raw_text, " ".repeat(60));

        assert_eq!(layout[1].char_start, 60);
        assert_eq!(layout[1].char_end, 120);
        assert_eq!(layout[1].raw_text, " ".repeat(60));

        assert_eq!(layout[2].char_start, 120);
        assert_eq!(layout[2].char_end, 130);
        assert_eq!(layout[2].raw_text, " ".repeat(10));
    }

    #[test]
    fn test_layout_empty_line_inherits_indent() {
        let config = Config::default();
        let lines = vec![
            "CHARLOTTE".to_string(),
            "Dialogue line".to_string(),
            "".to_string(),
        ];
        let types = vec![LineType::Character, LineType::Dialogue, LineType::Empty];

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());

        let empty_row = &layout[2];
        assert_eq!(empty_row.line_type, LineType::Empty);
        assert_eq!(empty_row.indent, LineType::Dialogue.fmt().indent);
    }

    #[test]
    fn test_layout_page_number_skips_empty_lines() {
        let config = Config::default();

        let mut lines = vec!["Text".to_string(); LINES_PER_PAGE];
        let mut types = vec![LineType::Action; LINES_PER_PAGE];

        lines.push("   ".to_string());
        types.push(LineType::Empty);

        lines.push("Real Text".to_string());
        types.push(LineType::Action);

        let layout = build_layout(&lines, &types, 999, &config, &Theme::default());

        let empty_row = layout
            .iter()
            .find(|r| r.line_type == LineType::Empty)
            .unwrap();

        assert_eq!(empty_row.page_num, None);

        let text_row = layout
            .iter()
            .skip_while(|r| r.line_type != LineType::Empty)
            .nth(1)
            .unwrap();
        assert_eq!(text_row.page_num, Some(2));
    }

    #[test]
    fn test_layout_soft_wrap_preserves_spaces_exactly() {
        let config = Config::default();

        let line = format!("Word{}Next", " ".repeat(58));
        let lines = vec![line];
        let types = vec![LineType::Action];

        let layout = build_layout(&lines, &types, 0, &config, &Theme::default());

        assert_eq!(
            layout.len(),
            3,
            "Line should wrap across 3 rows due to hard wrapping"
        );

        assert_eq!(layout[0].char_start, 0);
        assert_eq!(layout[0].char_end, 5);
        assert_eq!(layout[0].raw_text, "Word ");

        assert_eq!(layout[1].char_start, 5);
        assert_eq!(layout[1].char_end, 65);
        assert_eq!(layout[1].raw_text, format!("{}Nex", " ".repeat(57)));

        assert_eq!(layout[2].char_start, 65);
        assert_eq!(layout[2].char_end, 66);
        assert_eq!(layout[2].raw_text, "t".to_string());
    }

    #[test]
    fn test_layout_parenthetical_wrap_indent() {
        let config = Config::default();
        let lines = vec![
            "(this is a very long parenthetical that should wrap with a different indent)"
                .to_string(),
        ];
        let types = vec![LineType::Parenthetical];

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());

        assert!(layout.len() >= 2, "Parenthetical should wrap");

        assert_eq!(layout[0].indent, 16, "First line indent should be 16");
        assert_eq!(layout[1].indent, 17, "Wrapped line indent should be 17");
    }

    #[test]
    fn test_layout_tokenize_preserves_multiple_spaces() {
        let config = Config::default();
        let lines = vec!["A    B".to_string()];
        let types = vec![LineType::Action];

        let layout = build_layout(&lines, &types, 0, &config, &Theme::default());

        assert_eq!(layout.len(), 1);
        assert_eq!(
            layout[0].raw_text, "A    B",
            "Multiple spaces should not be collapsed"
        );
    }

    #[test]
    fn test_layout_active_line_with_markup_wraps_correctly() {
        let config = Config::default();
        let text = format!("**{}**", "a".repeat(60));
        let lines = vec![text];
        let types = vec![LineType::Action];

        let layout_active = build_layout(&lines, &types, 0, &config, &Theme::default());
        assert_eq!(
            layout_active.len(),
            2,
            "Active line should wrap because visible markup exceeds width"
        );

        let layout_inactive = build_layout(&lines, &types, 99, &config, &Theme::default());
        assert_eq!(
            layout_inactive.len(),
            1,
            "Inactive line should not wrap when markup is hidden"
        );
    }

    #[test]
    fn test_layout_phantom_lines_page_break_rollover() {
        let config = Config {
            heading_spacing: 10,
            ..Config::default()
        };
        let mut lines = vec!["Action".to_string(); crate::types::LINES_PER_PAGE - 2];
        let mut types = vec![LineType::Action; crate::types::LINES_PER_PAGE - 2];

        lines.push("INT. ROOM".to_string());
        types.push(LineType::SceneHeading);

        let layout = build_layout(&lines, &types, 999, &config, &Theme::default());
        let phantoms: Vec<_> = layout.iter().filter(|r| r.is_phantom).collect();

        assert!(phantoms.len() > 0);
    }

    #[test]
    fn test_strip_sigils_inline_note_in_heading() {
        let config = Config::default();
        let lines = vec![".HEADING [[yellow note]]".to_string()];
        let types = vec![LineType::SceneHeading];
        let layout = build_layout(&lines, &types, 999, &config, &Theme::default());

        assert_eq!(layout[0].raw_text, "HEADING");
        assert_eq!(
            layout[0].override_color,
            Some(ratatui::style::Color::Yellow)
        );
    }

    #[test]
    fn test_visual_to_logical_x_max_logical_break() {
        let row = VisualRow {
            line_idx: 0,
            char_start: 0,
            char_end: 2,
            raw_text: "AB".to_string(),
            line_type: LineType::Action,
            indent: 0,
            is_active: true,
            scene_num: None,
            page_num: None,
            override_color: None,
            fmt: Rc::new(LineFormatting::default()),
            is_phantom: false,
        };

        assert_eq!(row.visual_to_logical_x(100, false), 1);
    }

    #[test]
    fn test_layout_explicit_scene_numbers_logic() {
        let config = Config {
            show_scene_numbers: true,
            ..Config::default()
        };

        let lines = vec![
            "INT. ONE".to_string(),
            "INT. TWO #5#".to_string(),
            "INT. THREE".to_string(),
            "INT. FOUR #6A#".to_string(),
            "INT. FIVE".to_string(),
            "INT. SIX#10#".to_string(),
            "INT. SEVEN #B#".to_string(),
            "INT. EIGHT".to_string(),
        ];
        let types = vec![LineType::SceneHeading; 8];

        let layout = build_layout(&lines, &types, 99, &config, &Theme::default());
        let scenes: Vec<_> = layout
            .iter()
            .filter(|r| r.line_type == LineType::SceneHeading)
            .collect();

        assert_eq!(scenes[0].scene_num.as_deref(), Some("1"));
        assert_eq!(scenes[1].scene_num.as_deref(), Some("5"));
        assert_eq!(scenes[1].raw_text, "INT. TWO");
        assert_eq!(scenes[2].scene_num.as_deref(), Some("6"));
        assert_eq!(scenes[3].scene_num.as_deref(), Some("6A"));
        assert_eq!(scenes[3].raw_text, "INT. FOUR");
        assert_eq!(scenes[4].scene_num.as_deref(), Some("7"));
        assert_eq!(scenes[5].scene_num.as_deref(), Some("10"));
        assert_eq!(scenes[5].raw_text, "INT. SIX");
        assert_eq!(scenes[6].scene_num.as_deref(), Some("B"));
        assert_eq!(scenes[6].raw_text, "INT. SEVEN");
        assert_eq!(scenes[7].scene_num.as_deref(), Some("11"));
    }

    #[test]
    fn test_layout_explicit_scene_numbers_active_line() {
        let config = Config {
            show_scene_numbers: true,
            ..Config::default()
        };
        let lines = vec!["INT. KITCHEN #5#".to_string()];
        let types = vec![LineType::SceneHeading];
        let layout = build_layout(&lines, &types, 0, &config, &Theme::default());

        assert_eq!(layout[0].scene_num.as_deref(), Some("5"));
        assert_eq!(layout[0].raw_text, "INT. KITCHEN #5#");
    }
}

#[cfg(test)]
mod property_tests {
    use super::*;
    use proptest::prelude::*;

    fn any_line_type() -> impl Strategy<Value = LineType> {
        (0..15u8).prop_map(|idx| match idx {
            0 => LineType::Action,
            1 => LineType::SceneHeading,
            2 => LineType::Character,
            3 => LineType::Dialogue,
            4 => LineType::Parenthetical,
            5 => LineType::Transition,
            6 => LineType::Centered,
            7 => LineType::Lyrics,
            8 => LineType::Note,
            9 => LineType::Boneyard,
            10 => LineType::PageBreak,
            11 => LineType::MetadataKey,
            12 => LineType::MetadataValue,
            13 => LineType::Shot,
            _ => LineType::Empty,
        })
    }

    proptest! {
        #![proptest_config(ProptestConfig::with_cases(90000))]

        #[test]
        fn prop_formatting_parser_never_panics(s in "\\PC*") {
            let _fmt = parse_formatting(&s, &Theme::adaptive());
        }

        #[test]
        fn prop_sigil_stripping_is_safe_for_utf8(s in "[^\n]*", lt in any_line_type()) {
            let stripped = strip_sigils(&s, lt);
            let left_chars = sigil_left_chars(&s, lt);

            assert!(
                s.ends_with(stripped) || s.contains(stripped),
                "Stripped string must be a substring of the original"
            );

            assert!(
                left_chars <= s.chars().count(),
                "Sigil left chars exceeded total chars!"
            );
        }

        #[test]
        fn prop_layout_conserves_text_all_types(s in "[^\n]*", lt in any_line_type()) {
            let config = Config::default();
            let lines = vec![s.clone()];
            let types = vec![lt];

            let layout = build_layout(&lines, &types, 0, &config, &Theme::default());

            if !layout.is_empty() {
                let reconstructed: String = layout
                    .iter()
                    .filter(|r| !r.is_phantom)
                    .map(|r| r.raw_text.as_str())
                    .collect();

                assert_eq!(
                    s, reconstructed,
                    "Text conservation failed for type {:?}! Original vs Reconstructed differ.", lt
                );
            }
        }

        #[test]
        fn prop_layout_width_never_exceeds_limit(s in "[^\n]*", lt in any_line_type()) {
            let config = Config::default();
            let lines = vec![s];
            let types = vec![lt];
            let layout = build_layout(&lines, &types, 0, &config, &Theme::default());
            let max_width = lt.fmt().width;

            for row in layout.iter().filter(|r| !r.is_phantom) {
                let w = token_metrics(&row.raw_text, row.is_active, config.hide_markup).0;

                assert!(
                    w <= max_width,
                    "Row exceeded max width for type {:?}! Width: {}, Max: {}, Text: '{}'",
                    lt, w, max_width, row.raw_text
                );
            }
        }

        #[test]
        fn prop_char_boundaries_are_valid(s in "[^\n]*", lt in any_line_type()) {
            let config = Config::default();
            let lines = vec![s.clone()];
            let types = vec![lt];
            let layout = build_layout(&lines, &types, 0, &config, &Theme::default());

            let mut expected_start = 0;
            let total_chars = s.chars().count();

            for row in layout.iter().filter(|r| !r.is_phantom) {
                assert_eq!(
                    row.char_start, expected_start,
                    "Gap or overlap detected in char_start"
                );
                assert!(
                    row.char_end >= row.char_start,
                    "char_end cannot be less than char_start"
                );
                assert!(
                    row.char_end <= total_chars,
                    "char_end exceeded total characters"
                );

                let row_char_count = row.raw_text.chars().count();
                assert_eq!(
                    row.char_end - row.char_start, row_char_count,
                    "Mismatch between raw_text length and (char_end - char_start)"
                );

                expected_start = row.char_end;
            }

            if !layout.is_empty() {
                assert_eq!(
                    expected_start, total_chars,
                    "Final char_end did not reach the end of the string"
                );
            }
        }

        #[test]
        fn prop_cursor_roundtrip_never_panics(s in "[^\n]*", cursor_pos in 0usize..2000) {
            let config = Config::default();
            let lines = vec![s.clone()];
            let types = vec![LineType::Action];
            let layout = build_layout(&lines, &types, 0, &config, &Theme::default());

            let char_count = s.chars().count();
            let safe_cursor = if char_count == 0 { 0 } else { cursor_pos % (char_count + 1) };

            let (vi, visual_x) = find_visual_cursor(&layout, 0, safe_cursor);
            if vi < layout.len() {
                let row = &layout[vi];
                let is_last = row.char_end == char_count;
                let logical_back = row.visual_to_logical_x(visual_x, is_last);

                assert!(
                    logical_back <= char_count,
                    "visual_to_logical_x returned an out-of-bounds index: {} > {}",
                    logical_back, char_count
                );

                assert!(
                    logical_back >= row.char_start,
                    "Returned logical index is before the visual row start"
                );
            }
        }

        #[test]
        fn prop_to_uppercase_1to1_invariant(s in ".*") {
            use crate::formatting::StringCaseExt;
            let upper = s.to_uppercase_1to1();

            assert_eq!(
                s.chars().count(),
                upper.chars().count(),
                "to_uppercase_1to1 MUST strictly preserve character count"
            );
        }
    }
}
