use crate::model::shared::cursor::CursorMove;

use super::text_input::{TextInputLike, TextInputState, next_word_start, previous_word_start};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct LineSpan {
    start: usize,
    len: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct MultiLineDerivedState {
    line_spans: Vec<LineSpan>,
    cursor_row: usize,
    cursor_col: usize,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MultiLineInputState {
    inner: TextInputState,
    scroll_row: usize,
    preferred_col: Option<usize>,
    derived: MultiLineDerivedState,
}

impl MultiLineInputState {
    pub fn new(content: impl Into<String>, cursor: usize) -> Self {
        let inner = TextInputState::new(content, cursor);
        let derived = MultiLineDerivedState::new(inner.content(), inner.cursor());
        Self {
            inner,
            scroll_row: 0,
            preferred_col: None,
            derived,
        }
    }

    pub fn scroll_row(&self) -> usize {
        self.scroll_row
    }

    pub fn set_cursor(&mut self, pos: usize) {
        self.set_cursor_and_sync(pos);
        self.preferred_col = None;
    }

    pub fn insert_newline(&mut self) {
        self.insert_char('\n');
    }

    pub fn insert_tab(&mut self) {
        self.insert_str("    ");
    }

    pub fn insert_char(&mut self, c: char) {
        self.inner.insert_char(c);
        self.preferred_col = None;
        self.rebuild_derived();
    }

    pub fn insert_str(&mut self, text: &str) {
        self.inner.insert_str(text);
        self.preferred_col = None;
        self.rebuild_derived();
    }

    pub fn backspace(&mut self) {
        let previous_char_count = self.inner.char_count();
        self.inner.backspace();
        if self.inner.char_count() == previous_char_count {
            return;
        }
        self.preferred_col = None;
        self.rebuild_derived();
    }

    pub fn delete(&mut self) {
        let previous_char_count = self.inner.char_count();
        self.inner.delete();
        if self.inner.char_count() == previous_char_count {
            return;
        }
        self.preferred_col = None;
        self.rebuild_derived();
    }

    pub fn set_content(&mut self, s: String) {
        self.inner.set_content(s);
        self.scroll_row = 0;
        self.preferred_col = None;
        self.rebuild_derived();
    }

    pub fn set_content_with_cursor(&mut self, s: String, cursor: usize) {
        self.inner.set_content(s);
        // set_content resets the cursor to the end, so restore the requested position here.
        self.inner.set_cursor(cursor);
        self.scroll_row = 0;
        self.preferred_col = None;
        self.rebuild_derived();
    }

    pub fn clear(&mut self) {
        self.inner.clear();
        self.scroll_row = 0;
        self.preferred_col = None;
        self.rebuild_derived();
    }

    pub fn move_cursor(&mut self, movement: CursorMove) {
        match movement {
            CursorMove::Left | CursorMove::Right => {
                self.move_cursor_horizontally(movement);
                self.preferred_col = None;
            }
            CursorMove::Up => {
                let (current_line, current_col) = self.current_line_col();
                let preferred_col = self.preferred_col.unwrap_or(current_col);
                if current_line > 0 {
                    let previous = self.line_spans()[current_line - 1];
                    self.set_cursor_from_line(current_line - 1, preferred_col.min(previous.len));
                }
                self.preferred_col = Some(preferred_col);
            }
            CursorMove::Down => {
                let (current_line, current_col) = self.current_line_col();
                let preferred_col = self.preferred_col.unwrap_or(current_col);
                if current_line + 1 < self.line_spans().len() {
                    let next = self.line_spans()[current_line + 1];
                    self.set_cursor_from_line(current_line + 1, preferred_col.min(next.len));
                }
                self.preferred_col = Some(preferred_col);
            }
            CursorMove::Home | CursorMove::LineStart => {
                let (current_line, _) = self.current_line_col();
                self.set_cursor_from_line(current_line, 0);
                self.preferred_col = None;
            }
            CursorMove::End | CursorMove::LineEnd => {
                let (current_line, _) = self.current_line_col();
                let current = self.line_spans()[current_line];
                self.set_cursor_from_line(current_line, current.len);
                self.preferred_col = None;
            }
            CursorMove::WordForward => {
                let next = next_word_start(self.content(), self.cursor());
                self.set_cursor_and_sync(next);
                self.preferred_col = None;
            }
            CursorMove::WordBackward => {
                let previous = previous_word_start(self.content(), self.cursor());
                self.set_cursor_and_sync(previous);
                self.preferred_col = None;
            }
            CursorMove::BufferStart => {
                self.set_cursor_from_line(0, 0);
                self.preferred_col = None;
            }
            CursorMove::BufferEnd => {
                let last_row = self.line_spans().len().saturating_sub(1);
                let last = self.line_spans()[last_row];
                self.set_cursor_from_line(last_row, last.len);
                self.preferred_col = None;
            }
            CursorMove::FirstLine => {
                let (_, current_col) = self.current_line_col();
                let preferred_col = self.preferred_col.unwrap_or(current_col);
                let first = self.line_spans()[0];
                self.set_cursor_from_line(0, preferred_col.min(first.len));
                self.preferred_col = Some(preferred_col);
            }
            CursorMove::LastLine => {
                let (_, current_col) = self.current_line_col();
                let preferred_col = self.preferred_col.unwrap_or(current_col);
                let last_row = self.line_spans().len().saturating_sub(1);
                let last = self.line_spans()[last_row];
                self.set_cursor_from_line(last_row, preferred_col.min(last.len));
                self.preferred_col = Some(preferred_col);
            }
            CursorMove::ViewportTop | CursorMove::ViewportMiddle | CursorMove::ViewportBottom => {}
        }
    }

    pub fn move_cursor_to_viewport_position(&mut self, movement: CursorMove, visible_rows: usize) {
        if visible_rows == 0 {
            return;
        }

        let (_, current_col) = self.current_line_col();
        let preferred_col = self.preferred_col.unwrap_or(current_col);
        let target_row = match movement {
            CursorMove::ViewportTop => self.scroll_row,
            CursorMove::ViewportMiddle => self.scroll_row + visible_rows.saturating_sub(1) / 2,
            CursorMove::ViewportBottom => self.scroll_row + visible_rows.saturating_sub(1),
            _ => return,
        }
        .min(self.line_spans().len().saturating_sub(1));

        let target = self.line_spans()[target_row];
        self.set_cursor_from_line(target_row, preferred_col.min(target.len));
        self.preferred_col = Some(preferred_col);
    }

    pub fn cursor_to_position(&self) -> (usize, usize) {
        (self.derived.cursor_row, self.derived.cursor_col)
    }

    pub fn update_scroll(&mut self, visible_rows: usize) {
        if visible_rows == 0 {
            return;
        }
        if self.derived.cursor_row < self.scroll_row {
            self.scroll_row = self.derived.cursor_row;
        } else if self.derived.cursor_row >= self.scroll_row + visible_rows {
            self.scroll_row = self.derived.cursor_row - visible_rows + 1;
        }
    }

    pub fn char_to_byte_index(&self, char_idx: usize) -> usize {
        char_to_byte_index_impl(self.content(), char_idx)
    }

    fn rebuild_derived(&mut self) {
        self.derived = MultiLineDerivedState::new(self.content(), self.cursor());
    }

    fn current_line_col(&self) -> (usize, usize) {
        (self.derived.cursor_row, self.derived.cursor_col)
    }

    fn line_spans(&self) -> &[LineSpan] {
        debug_assert!(!self.derived.line_spans.is_empty());
        &self.derived.line_spans
    }

    fn move_cursor_horizontally(&mut self, movement: CursorMove) {
        let previous_cursor = self.cursor();
        self.inner.move_cursor(movement);
        if self.cursor() == previous_cursor {
            return;
        }

        match movement {
            CursorMove::Left => {
                if self.derived.cursor_col > 0 {
                    self.derived.cursor_col -= 1;
                } else if self.derived.cursor_row > 0 {
                    self.derived.cursor_row -= 1;
                    self.derived.cursor_col = self.line_spans()[self.derived.cursor_row].len;
                }
            }
            CursorMove::Right => {
                let current = self.line_spans()[self.derived.cursor_row];
                if self.derived.cursor_col < current.len {
                    self.derived.cursor_col += 1;
                } else if self.derived.cursor_row + 1 < self.line_spans().len() {
                    self.derived.cursor_row += 1;
                    self.derived.cursor_col = 0;
                }
            }
            _ => unreachable!("horizontal helper only supports left/right"),
        }
    }

    fn set_cursor_raw(&mut self, pos: usize) {
        let clamped = pos.min(self.char_count());
        // viewport reset by set_cursor is acceptable: MultiLineInputState doesn't use inner's viewport
        self.inner.set_cursor(clamped);
    }

    fn set_cursor_from_line(&mut self, row: usize, col: usize) {
        let span = self.line_spans()[row];
        let clamped_col = col.min(span.len);
        self.set_cursor_raw(span.start + clamped_col);
        self.derived.cursor_row = row;
        self.derived.cursor_col = clamped_col;
    }

    fn set_cursor_and_sync(&mut self, pos: usize) {
        self.set_cursor_raw(pos);
        let (row, col) = find_cursor_position(self.line_spans(), self.cursor());
        self.derived.cursor_row = row;
        self.derived.cursor_col = col;
    }
}

impl TextInputLike for MultiLineInputState {
    fn text_input(&self) -> &TextInputState {
        &self.inner
    }
}

impl MultiLineDerivedState {
    fn default_empty_line() -> Self {
        Self {
            line_spans: vec![LineSpan::default()],
            cursor_row: 0,
            cursor_col: 0,
        }
    }

    fn new(content: &str, cursor: usize) -> Self {
        let line_spans = build_line_spans(content);
        let (cursor_row, cursor_col) = find_cursor_position(&line_spans, cursor);
        Self {
            line_spans,
            cursor_row,
            cursor_col,
        }
    }
}

impl Default for MultiLineDerivedState {
    fn default() -> Self {
        Self::default_empty_line()
    }
}

fn build_line_spans(content: &str) -> Vec<LineSpan> {
    let mut line_spans = Vec::new();
    let mut start = 0;
    for line in content.split('\n') {
        let len = line.chars().count();
        line_spans.push(LineSpan { start, len });
        start += len + 1;
    }
    line_spans
}

fn find_cursor_position(line_spans: &[LineSpan], cursor: usize) -> (usize, usize) {
    let row = line_spans
        .partition_point(|span| span.start <= cursor)
        .saturating_sub(1);
    let span = line_spans.get(row).copied().unwrap_or_default();
    (row, cursor.saturating_sub(span.start).min(span.len))
}

fn char_to_byte_index_impl(s: &str, char_idx: usize) -> usize {
    s.char_indices()
        .nth(char_idx)
        .map_or(s.len(), |(byte_idx, _)| byte_idx)
}

#[cfg(test)]
mod perf_tests;

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

    fn ml(content: &str, cursor: usize) -> MultiLineInputState {
        MultiLineInputState::new(content, cursor)
    }

    mod cursor_to_position {
        use super::*;

        mod position {
            use super::*;

            #[test]
            fn empty_string_returns_origin() {
                let s = ml("", 0);
                assert_eq!(s.cursor_to_position(), (0, 0));
            }

            #[test]
            fn single_line_returns_correct_col() {
                let s = ml("SELECT * FROM users", 7);
                assert_eq!(s.cursor_to_position(), (0, 7));
            }

            #[test]
            fn multiline_returns_correct_row_and_col() {
                // "SELECT *\nFROM users\nWHERE id = 1"
                //  cursor at 17 → "FROM user" (8 chars of line0 + \n + 8 chars into line1)
                let s = ml("SELECT *\nFROM users\nWHERE id = 1", 17);
                assert_eq!(s.cursor_to_position(), (1, 8));
            }

            #[rstest]
            #[case("こんにちは\n世界", 5, (0, 5))]
            #[case("こんにちは\n世界", 6, (1, 0))]
            #[case("こんにちは\n世界", 7, (1, 1))]
            fn multibyte_returns_correct_row_and_col(
                #[case] content: &str,
                #[case] cursor: usize,
                #[case] expected: (usize, usize),
            ) {
                let s = ml(content, cursor);
                assert_eq!(s.cursor_to_position(), expected);
            }
        }

        mod newline_boundary {
            use super::*;

            #[test]
            fn trailing_newline_returns_next_row_origin() {
                // "abc\n" → 2 lines: ("abc", 3) and ("", 0)
                // cursor at 4 → line 1, col 0
                let s = ml("abc\n", 4);
                assert_eq!(s.cursor_to_position(), (1, 0));
            }

            #[test]
            fn consecutive_newlines_returns_middle_row() {
                // "a\n\nb" → lines: ("a",1), ("",0), ("b",1)
                // cursor at 2 → line 1, col 0
                let s = ml("a\n\nb", 2);
                assert_eq!(s.cursor_to_position(), (1, 0));
            }

            #[test]
            fn cursor_before_newline_returns_end_of_current_line() {
                // "abc\ndef" → cursor at 3 (on \n boundary, actually end of line 0)
                let s = ml("abc\ndef", 3);
                assert_eq!(s.cursor_to_position(), (0, 3));
            }

            #[test]
            fn cursor_after_newline_returns_start_of_next_line() {
                // "abc\ndef" → cursor at 4 (start of line 1)
                let s = ml("abc\ndef", 4);
                assert_eq!(s.cursor_to_position(), (1, 0));
            }
        }
    }

    mod move_cursor {
        use super::*;

        mod horizontal {
            use super::*;

            #[test]
            fn left_right_moves_cursor_by_one() {
                let mut s = ml("abc", 1);
                s.move_cursor(CursorMove::Left);
                assert_eq!(s.cursor(), 0);
                s.move_cursor(CursorMove::Right);
                assert_eq!(s.cursor(), 1);
            }

            #[test]
            fn left_at_start_returns_zero() {
                let mut s = ml("abc", 0);
                s.move_cursor(CursorMove::Left);
                assert_eq!(s.cursor(), 0);
            }

            #[test]
            fn right_at_end_returns_unchanged() {
                let mut s = ml("abc", 3);
                s.move_cursor(CursorMove::Right);
                assert_eq!(s.cursor(), 3);
            }
        }

        mod vertical {
            use super::*;

            #[test]
            fn up_from_second_line_returns_same_col_in_first() {
                // "abc\ndef" → cursor at 5 (d=4, e=5) → col=1
                // Up → line 0, col 1 → cursor=1
                let mut s = ml("abc\ndef", 5);
                s.move_cursor(CursorMove::Up);
                assert_eq!(s.cursor(), 1);
            }

            #[test]
            fn up_from_first_line_returns_unchanged() {
                let mut s = ml("abc\ndef", 1);
                s.move_cursor(CursorMove::Up);
                assert_eq!(s.cursor(), 1);
            }

            #[test]
            fn down_from_first_line_returns_same_col_in_second() {
                // "abc\ndef" → cursor at 1 → col=1
                // Down → line 1, col 1 → cursor=5
                let mut s = ml("abc\ndef", 1);
                s.move_cursor(CursorMove::Down);
                assert_eq!(s.cursor(), 5);
            }

            #[test]
            fn down_from_last_line_returns_unchanged() {
                let mut s = ml("abc\ndef", 5);
                s.move_cursor(CursorMove::Down);
                assert_eq!(s.cursor(), 5);
            }

            #[test]
            fn up_clamps_col_to_shorter_line_length() {
                // "ab\ncdef" → cursor at 7 (end of "cdef"), col=4
                // Up → line 0 has len 2, so col clamped to 2 → cursor=2
                let mut s = ml("ab\ncdef", 7);
                s.move_cursor(CursorMove::Up);
                assert_eq!(s.cursor(), 2);
            }

            #[test]
            fn down_clamps_col_to_shorter_line_length() {
                // "cdef\nab" → cursor at 4 (end of "cdef"), col=4
                // Down → line 1 has len 2, so col clamped to 2 → cursor=7
                let mut s = ml("cdef\nab", 4);
                s.move_cursor(CursorMove::Down);
                assert_eq!(s.cursor(), 7);
            }

            #[test]
            fn up_from_empty_trailing_line_returns_prev_line_origin() {
                // "abc\n" → cursor at 4 (empty line 1)
                // Up → line 0, col 0.min(3) = 0 → cursor=0
                let mut s = ml("abc\n", 4);
                s.move_cursor(CursorMove::Up);
                assert_eq!(s.cursor(), 0);
            }

            #[test]
            fn down_to_empty_trailing_line_returns_next_row_origin() {
                // "abc\n" → cursor at 2 (col=2)
                // Down → line 1, col 2.min(0) = 0 → cursor=4
                let mut s = ml("abc\n", 2);
                s.move_cursor(CursorMove::Down);
                assert_eq!(s.cursor(), 4);
            }

            #[test]
            fn up_through_empty_line_restores_preferred_column() {
                // "abc\n\ndef" → lines: (0,3), (4,0), (5,3)
                // Start at cursor=6 (line 2, col 1 → 'e')
                let mut s = ml("abc\n\ndef", 6);
                assert_eq!(s.cursor_to_position(), (2, 1));

                // Up → line 1 (empty), col 1.min(0) = 0 → cursor=4
                s.move_cursor(CursorMove::Up);
                assert_eq!(s.cursor(), 4);
                assert_eq!(s.cursor_to_position(), (1, 0));

                // Up again → line 0, restore preferred col 1 → cursor=1
                s.move_cursor(CursorMove::Up);
                assert_eq!(s.cursor(), 1);
                assert_eq!(s.cursor_to_position(), (0, 1));
            }

            #[test]
            fn multibyte_up_down_preserves_col() {
                // "あいう\nかき" → lines: (0,3), (4,2)
                // cursor at 5 (line 1, col 1 → 'き')
                let mut s = ml("あいう\nかき", 5);
                assert_eq!(s.cursor_to_position(), (1, 1));

                // Up → line 0, col 1.min(3) = 1 → cursor=1
                s.move_cursor(CursorMove::Up);
                assert_eq!(s.cursor(), 1);

                // Down → line 1, col 1.min(2) = 1 → cursor=5
                s.move_cursor(CursorMove::Down);
                assert_eq!(s.cursor(), 5);
            }
        }

        mod line_boundary {
            use super::*;

            #[rstest]
            #[case(CursorMove::Home)]
            #[case(CursorMove::LineStart)]
            fn returns_current_line_start(#[case] movement: CursorMove) {
                // "abc\ndef" → cursor at 5 (on 'e'), col=1
                // Home → start of line 1 → cursor=4
                let mut s = ml("abc\ndef", 5);
                s.move_cursor(movement);
                assert_eq!(s.cursor(), 4);
            }

            #[rstest]
            #[case(CursorMove::End)]
            #[case(CursorMove::LineEnd)]
            fn returns_current_line_end(#[case] movement: CursorMove) {
                // "abc\ndef" → cursor at 4 (on 'd'), col=0
                // End → end of line 1 → cursor=7
                let mut s = ml("abc\ndef", 4);
                s.move_cursor(movement);
                assert_eq!(s.cursor(), 7);
            }

            #[rstest]
            #[case(CursorMove::Home)]
            #[case(CursorMove::LineStart)]
            fn first_line_start_returns_zero(#[case] movement: CursorMove) {
                let mut s = ml("abc\ndef", 2);
                s.move_cursor(movement);
                assert_eq!(s.cursor(), 0);
            }

            #[rstest]
            #[case(CursorMove::End)]
            #[case(CursorMove::LineEnd)]
            fn first_line_end_returns_line_length(#[case] movement: CursorMove) {
                let mut s = ml("abc\ndef", 0);
                s.move_cursor(movement);
                assert_eq!(s.cursor(), 3);
            }

            #[rstest]
            #[case(CursorMove::Home, 4)]
            #[case(CursorMove::LineStart, 4)]
            #[case(CursorMove::End, 4)]
            #[case(CursorMove::LineEnd, 4)]
            fn empty_line_returns_same_position(
                #[case] movement: CursorMove,
                #[case] expected: usize,
            ) {
                // "abc\n\ndef" → cursor at 4 (empty line 1)
                let mut s = ml("abc\n\ndef", 4);

                s.move_cursor(movement);
                assert_eq!(s.cursor(), expected);
            }

            #[rstest]
            #[case(CursorMove::Home, 4)]
            #[case(CursorMove::LineStart, 4)]
            #[case(CursorMove::End, 6)]
            #[case(CursorMove::LineEnd, 6)]
            fn multibyte_returns_line_boundaries(
                #[case] movement: CursorMove,
                #[case] expected: usize,
            ) {
                let mut s = ml("あいう\nかき", 5);

                s.move_cursor(movement);
                assert_eq!(s.cursor(), expected);
            }
        }

        mod word {
            use super::*;

            #[test]
            fn forward_moves_to_start_of_next_word() {
                let mut s = ml("SELECT users", 0);
                s.move_cursor(CursorMove::WordForward);
                assert_eq!(s.cursor(), 7);
            }

            #[test]
            fn backward_moves_to_start_of_current_word() {
                let mut s = ml("SELECT users", 10);
                s.move_cursor(CursorMove::WordBackward);
                assert_eq!(s.cursor(), 7);
            }

            #[test]
            fn forward_crosses_punctuation_boundary() {
                let mut s = ml("foo(bar)", 0);
                s.move_cursor(CursorMove::WordForward);
                assert_eq!(s.cursor(), 3);

                s.move_cursor(CursorMove::WordForward);
                assert_eq!(s.cursor(), 4);
            }

            #[test]
            fn backward_crosses_punctuation_boundary() {
                let mut s = ml("foo(bar)", 7);
                s.move_cursor(CursorMove::WordBackward);
                assert_eq!(s.cursor(), 4);

                s.move_cursor(CursorMove::WordBackward);
                assert_eq!(s.cursor(), 3);
            }

            #[test]
            fn forward_crosses_whitespace_and_newline() {
                let mut s = ml("foo \n  bar", 0);
                s.move_cursor(CursorMove::WordForward);
                assert_eq!(s.cursor(), 7);
            }

            #[test]
            fn forward_treats_cjk_as_keyword_word() {
                let mut s = ml("SELECT あいう", 0);
                s.move_cursor(CursorMove::WordForward);
                assert_eq!(s.cursor(), 7);

                s.move_cursor(CursorMove::WordForward);
                assert_eq!(s.cursor(), 10);
            }

            #[test]
            fn backward_crosses_whitespace_and_newline() {
                let mut s = ml("foo \n  bar", 10);
                s.move_cursor(CursorMove::WordBackward);
                assert_eq!(s.cursor(), 7);

                s.move_cursor(CursorMove::WordBackward);
                assert_eq!(s.cursor(), 0);
            }

            #[test]
            fn forward_at_end_returns_unchanged() {
                let mut s = ml("foo", 3);
                s.move_cursor(CursorMove::WordForward);
                assert_eq!(s.cursor(), 3);
            }

            #[test]
            fn backward_at_start_returns_unchanged() {
                let mut s = ml("foo", 0);
                s.move_cursor(CursorMove::WordBackward);
                assert_eq!(s.cursor(), 0);
            }
        }

        mod buffer_navigation {
            use super::*;

            #[test]
            fn start_returns_position_zero() {
                let mut s = ml("abc\ndef", 5);
                s.move_cursor(CursorMove::BufferStart);
                assert_eq!(s.cursor(), 0);
            }

            #[test]
            fn end_returns_last_position() {
                let mut s = ml("abc\ndef", 1);
                s.move_cursor(CursorMove::BufferEnd);
                assert_eq!(s.cursor(), 7);
            }

            #[test]
            fn first_line_preserves_column() {
                let mut s = ml("abcd\nxy\nwxyz12", 10);
                s.move_cursor(CursorMove::FirstLine);
                assert_eq!(s.cursor_to_position(), (0, 2));
            }

            #[test]
            fn first_line_clamps_to_line_length() {
                let mut s = ml("xy\nabcdef", 8);
                s.move_cursor(CursorMove::FirstLine);
                assert_eq!(s.cursor_to_position(), (0, 2));
            }

            #[test]
            fn last_line_preserves_column() {
                let mut s = ml("abcd\nxy\nwxyz12", 2);
                s.move_cursor(CursorMove::LastLine);
                assert_eq!(s.cursor_to_position(), (2, 2));
            }

            #[test]
            fn last_line_clamps_to_line_length() {
                let mut s = ml("abcdef\nxy", 5);
                s.move_cursor(CursorMove::LastLine);
                assert_eq!(s.cursor_to_position(), (1, 2));
            }

            #[test]
            fn preferred_column_restored_long_to_short_line() {
                let mut s = ml("abcdefghij\nxy", 5);

                s.move_cursor(CursorMove::LastLine);
                assert_eq!(s.cursor_to_position(), (1, 2));

                s.move_cursor(CursorMove::FirstLine);
                assert_eq!(s.cursor_to_position(), (0, 5));
            }

            #[test]
            fn preferred_column_restored_short_to_long_line() {
                let mut s = ml("xy\nabcdefghij", 8);

                s.move_cursor(CursorMove::FirstLine);
                assert_eq!(s.cursor_to_position(), (0, 2));

                s.move_cursor(CursorMove::LastLine);
                assert_eq!(s.cursor_to_position(), (1, 5));
            }
        }
    }

    mod edit {
        use super::*;

        #[test]
        fn insert_newline_splits_content() {
            let mut s = ml("abcdef", 3);
            s.insert_newline();
            assert_eq!(s.content(), "abc\ndef");
            assert_eq!(s.cursor(), 4);
            assert_eq!(s.cursor_to_position(), (1, 0));
        }

        #[test]
        fn insert_tab_adds_four_spaces() {
            let mut s = ml("abc", 3);
            s.insert_tab();
            assert_eq!(s.content(), "abc    ");
            assert_eq!(s.cursor(), 7);
        }

        #[test]
        fn backspace_at_newline_joins_adjacent_lines() {
            // "abc\ndef" → cursor at 4 (start of "def")
            // backspace removes \n → "abcdef", cursor=3
            let mut s = ml("abc\ndef", 4);
            s.backspace();
            assert_eq!(s.content(), "abcdef");
            assert_eq!(s.cursor(), 3);
        }

        #[test]
        fn backspace_at_buffer_start_is_noop() {
            let mut s = ml("abc", 0);

            s.backspace();

            assert_eq!(s.content(), "abc");
            assert_eq!(s.cursor_to_position(), (0, 0));
        }

        #[test]
        fn delete_at_newline_joins_adjacent_lines() {
            // "abc\ndef" → cursor at 3 (end of "abc", on \n)
            // delete removes \n → "abcdef", cursor=3
            let mut s = ml("abc\ndef", 3);
            s.delete();
            assert_eq!(s.content(), "abcdef");
            assert_eq!(s.cursor(), 3);
        }

        #[test]
        fn delete_at_buffer_end_is_noop() {
            let mut s = ml("abc", 3);

            s.delete();

            assert_eq!(s.content(), "abc");
            assert_eq!(s.cursor_to_position(), (0, 3));
        }

        #[test]
        fn clears_preferred_column() {
            let mut s = ml("abcdefghij\nxy\nabcdefghij", 8);

            s.move_cursor(CursorMove::Down);
            assert_eq!(s.cursor_to_position(), (1, 2));

            s.insert_char('z');
            s.move_cursor(CursorMove::Down);

            assert_eq!(s.cursor_to_position(), (2, 3));
        }

        #[test]
        fn backspace_rebuilds_cached_cursor_position() {
            let mut s = ml("abc\ndef", 4);

            s.backspace();

            assert_eq!(s.content(), "abcdef");
            assert_eq!(s.cursor_to_position(), (0, 3));
        }
    }

    mod viewport_position {
        use super::*;

        #[test]
        fn top_preserves_column() {
            let mut s = ml("aa\nbb\ncc\ndd", 10);
            s.scroll_row = 1;
            s.move_cursor_to_viewport_position(CursorMove::ViewportTop, 3);
            assert_eq!(s.cursor(), 4);
        }

        #[test]
        fn middle_preserves_column() {
            let mut s = ml("aa\nbb\ncc\ndd\nee", 13);
            s.scroll_row = 1;
            s.move_cursor_to_viewport_position(CursorMove::ViewportMiddle, 3);
            assert_eq!(s.cursor(), 7);
        }

        #[test]
        fn bottom_preserves_column() {
            let mut s = ml("aa\nbb\ncc\ndd\nee", 1);
            s.scroll_row = 1;
            s.move_cursor_to_viewport_position(CursorMove::ViewportBottom, 3);
            assert_eq!(s.cursor(), 10);
        }
    }

    mod scroll {
        use super::*;

        #[test]
        fn within_viewport_returns_unchanged() {
            let mut s = ml("line1\nline2\nline3", 0);
            s.update_scroll(3);
            assert_eq!(s.scroll_row(), 0);
        }

        #[test]
        fn below_viewport_advances() {
            // cursor on line 2 (index 2), visible_rows=2, scroll should advance
            let mut s = ml("line1\nline2\nline3", 12); // "line3" start
            s.update_scroll(2);
            assert_eq!(s.scroll_row(), 1); // row 2 - 2 + 1 = 1
        }

        #[test]
        fn above_viewport_retreats() {
            let mut s = ml("line1\nline2\nline3", 0);
            s.scroll_row = 2;
            s.update_scroll(2);
            assert_eq!(s.scroll_row(), 0);
        }

        #[test]
        fn zero_visible_rows_returns_unchanged() {
            let mut s = ml("line1\nline2", 6);
            s.scroll_row = 1;
            s.update_scroll(0);
            assert_eq!(s.scroll_row(), 1); // unchanged
        }
    }

    mod content_management {
        use super::*;

        #[test]
        fn default_preserves_single_empty_line_invariant() {
            let mut s = MultiLineInputState::default();

            s.move_cursor(CursorMove::LineEnd);

            assert_eq!(s.cursor(), 0);
            assert_eq!(s.cursor_to_position(), (0, 0));
        }

        #[test]
        fn set_content_resets_scroll_and_sets_cursor_to_end() {
            let mut s = ml("old\ncontent", 3);
            s.scroll_row = 5;

            s.set_content("new".to_string());

            assert_eq!(s.content(), "new");
            assert_eq!(s.cursor(), 3);
            assert_eq!(s.scroll_row(), 0);
        }

        #[test]
        fn set_content_with_cursor_sets_exact_position() {
            let mut s = ml("old\ncontent", 3);
            s.scroll_row = 5;

            s.set_content_with_cursor("new\nvalue".to_string(), 4);

            assert_eq!(s.content(), "new\nvalue");
            assert_eq!(s.cursor(), 4);
            assert_eq!(s.scroll_row(), 0);
            assert_eq!(s.cursor_to_position(), (1, 0));
        }

        #[test]
        fn set_content_with_cursor_clamps_past_end() {
            let mut s = ml("x", 0);

            s.set_content_with_cursor("ab".to_string(), 100);

            assert_eq!(s.cursor(), 2);
        }

        #[test]
        fn set_cursor_syncs_cached_position() {
            let mut s = ml("aa\nbb\ncc", 0);

            s.set_cursor(4);

            assert_eq!(s.cursor(), 4);
            assert_eq!(s.cursor_to_position(), (1, 1));
        }

        #[test]
        fn clear_resets_all_fields() {
            let mut s = ml("multi\nline", 8);
            s.scroll_row = 3;

            s.clear();

            assert_eq!(s.content(), "");
            assert_eq!(s.cursor(), 0);
            assert_eq!(s.scroll_row(), 0);
        }
    }

    mod byte_index {
        use super::*;

        #[test]
        fn ascii_returns_same_index() {
            let s = ml("abcdef", 0);
            assert_eq!(s.char_to_byte_index(3), 3);
        }

        #[test]
        fn multibyte_returns_correct_byte_indices() {
            let s = ml("あいう", 0);
            // each hiragana is 3 bytes
            assert_eq!(s.char_to_byte_index(1), 3);
            assert_eq!(s.char_to_byte_index(2), 6);
        }

        #[test]
        fn past_end_returns_content_byte_len() {
            let s = ml("abc", 0);
            assert_eq!(s.char_to_byte_index(100), 3);
        }
    }
}
