use crate::common::harness::EditorTestHarness;
use crossterm::event::{KeyCode, KeyModifiers};
use fresh::config::{Config, LanguageConfig};

/// Test basic line wrapping rendering
#[test]
fn test_line_wrapping_basic_rendering() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Type a long line that will wrap
    let long_text = "This is a very long line of text that will definitely exceed the terminal width and should wrap to multiple lines when line wrapping is enabled.";
    harness.type_text(long_text).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // The text should be visible in the screen output
    // Since it wraps, we should see parts of the text on multiple lines
    // Note: the exact visible portion depends on viewport and wrapping
    assert!(
        screen.contains("line wrapping")
            || screen.contains("terminal")
            || screen.contains("multiple"),
        "Screen should contain some part of the wrapped text"
    );

    // Buffer content should still be a single line (no newlines added)
    assert_eq!(harness.get_buffer_content().unwrap(), long_text);
    assert!(!harness.get_buffer_content().unwrap().contains('\n'));
}

/// Test line wrapping can be disabled
#[test]
fn test_line_wrapping_disabled() {
    let config = Config {
        editor: fresh::config::EditorConfig {
            line_wrap: false,
            ..Default::default()
        },
        ..Default::default()
    };
    let mut harness = EditorTestHarness::with_config(60, 24, config).unwrap();

    // Type a long line
    let long_text = "This is a very long line of text that will definitely exceed the terminal width and would normally wrap but should not when disabled.";
    harness.type_text(long_text).unwrap();
    harness.render().unwrap();

    // Buffer content should still be a single line
    assert_eq!(harness.get_buffer_content().unwrap(), long_text);
    assert!(!harness.get_buffer_content().unwrap().contains('\n'));
}

/// Test cursor navigation with wrapped lines - Home key goes to visual line start
#[test]
fn test_wrapped_line_navigation_home() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Type a long line that will wrap
    // With 60-col terminal, gutter=8, scrollbar=1, text width=51
    let long_text = "This is a very long line of text that will definitely exceed the terminal width and should wrap to multiple lines.";
    harness.type_text(long_text).unwrap();

    // Cursor should be at the end (on last wrapped segment)
    assert_eq!(harness.cursor_position(), long_text.len());

    // Render to populate cached layout
    harness.render().unwrap();

    // Press Home - should go to start of the visual (wrapped) segment the cursor is on
    harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();

    // The cursor was on the last wrapped segment; Home should move to start of that segment,
    // not to position 0 (which is on the first segment)
    let pos_after_home = harness.cursor_position();
    assert!(
        pos_after_home > 0,
        "Home from last wrapped segment should go to start of that visual segment, not position 0. Got {}",
        pos_after_home
    );

    // Keep pressing Home until we reach physical line start
    let mut pos = pos_after_home;
    while pos > 0 {
        harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
        let new_pos = harness.cursor_position();
        assert!(
            new_pos < pos,
            "Home should make progress toward line start. Was {}, now {}",
            pos,
            new_pos
        );
        pos = new_pos;
    }
    assert_eq!(
        pos, 0,
        "Repeated Home presses should reach physical line start"
    );
}

/// Test cursor navigation with wrapped lines - End key goes to visual line end
#[test]
fn test_wrapped_line_navigation_end() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Type a long line that will wrap
    // With 60-col terminal, gutter=8, scrollbar=1, text width=51
    let long_text = "This is a very long line of text that will definitely exceed the terminal width and should wrap to multiple lines.";
    harness.type_text(long_text).unwrap();

    // Move to document start (Ctrl+Home) to reliably get to position 0
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    assert_eq!(harness.cursor_position(), 0);

    // Render to populate cached layout
    harness.render().unwrap();

    // Press End - should go to end of the VISUAL line (first wrapped segment), not the physical line end
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();

    let pos_after_end = harness.cursor_position();
    // Should NOT be at the end of the entire line
    assert!(
        pos_after_end < long_text.len(),
        "End from first wrapped segment should go to end of visual segment, not end of physical line. Got {} (full length {})",
        pos_after_end,
        long_text.len()
    );

    // Pressing End again should go to end of the physical line
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    // Now we're on the next segment; End goes to its end.
    // Keep pressing End until we reach physical line end
    let mut pos = harness.cursor_position();
    while pos < long_text.len() {
        harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
        let new_pos = harness.cursor_position();
        assert!(
            new_pos > pos,
            "End key should make progress toward line end"
        );
        pos = new_pos;
    }
    assert_eq!(
        pos,
        long_text.len(),
        "Repeated End presses should eventually reach physical line end"
    );
}

/// Test cursor navigation with wrapped lines - Left/Right arrows
#[test]
fn test_wrapped_line_navigation_arrows() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Type a long line that will wrap
    harness.type_text("This is a very long line of text that will definitely exceed the terminal width and should wrap to multiple lines.").unwrap();

    let end_pos = harness.cursor_position();

    // Press Left arrow multiple times
    for _ in 0..10 {
        harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
    }

    // Cursor should have moved left by 10 positions
    assert_eq!(harness.cursor_position(), end_pos - 10);

    // Press Right arrow to move back
    for _ in 0..5 {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
    }

    // Cursor should have moved right by 5 positions
    assert_eq!(harness.cursor_position(), end_pos - 5);
}

/// Test editing in the middle of a wrapped line
#[test]
fn test_wrapped_line_editing_middle() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Type a long line
    let long_text = "This is a very long line of text that will definitely exceed the terminal width and should wrap.";
    harness.type_text(long_text).unwrap();

    // Move to the start of the line using Ctrl+Home (document start)
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    for _ in 0..20 {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
    }

    assert_eq!(harness.cursor_position(), 20);

    // Insert text in the middle
    harness.type_text("[INSERTED]").unwrap();

    // Verify the text was inserted correctly
    let expected = "This is a very long [INSERTED]line of text that will definitely exceed the terminal width and should wrap.";
    assert_eq!(harness.get_buffer_content().unwrap(), expected);

    // Cursor should be after the inserted text
    assert_eq!(harness.cursor_position(), 30); // 20 + 10
}

/// Test multiple wrapped lines
#[test]
fn test_multiple_wrapped_lines() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Type multiple long lines
    harness.type_text("First very long line that will wrap across multiple display lines in the terminal window.").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness
        .type_text(
            "Second very long line that will also wrap and take up multiple rows in the display.",
        )
        .unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text("Third line is shorter.").unwrap();

    harness.render().unwrap();

    let buffer_content = harness.get_buffer_content().unwrap();

    // Should have exactly 2 newlines (3 logical lines)
    assert_eq!(buffer_content.matches('\n').count(), 2);

    // Should contain all our text
    assert!(buffer_content.contains("First very long line"));
    assert!(buffer_content.contains("Second very long line"));
    assert!(buffer_content.contains("Third line is shorter"));
}

/// Test Up/Down navigation with wrapped lines
#[test]
fn test_wrapped_line_navigation_up_down() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Create two lines, first one wraps
    harness.type_text("This is a very long first line that will wrap to multiple display lines in the terminal.").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text("Short second line.").unwrap();

    let second_line_end = harness.cursor_position();

    // Press Up - should go to first line
    harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();

    let first_line_pos = harness.cursor_position();

    // Should be on the first line (before the newline)
    assert!(first_line_pos < second_line_end);

    // Press Down - should go back to second line
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();

    // Should be back near the end of second line
    assert!(harness.cursor_position() >= second_line_end - 20);
}

/// Test deleting content in wrapped lines
#[test]
fn test_wrapped_line_deletion() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Type a long line
    harness
        .type_text("This is a very long line that will wrap to multiple display lines.")
        .unwrap();

    let initial_len = harness.get_buffer_content().unwrap().len();

    // Delete some characters with backspace
    for _ in 0..10 {
        harness
            .send_key(KeyCode::Backspace, KeyModifiers::NONE)
            .unwrap();
    }

    // Content should be shorter
    assert_eq!(
        harness.get_buffer_content().unwrap().len(),
        initial_len - 10
    );

    let content = harness.get_buffer_content().unwrap();

    // Should end with "disp" now (removed "lay lines.")
    // Original: "This is a very long line that will wrap to multiple display lines."
    // After removing 10 chars (from "lay lines."): "This is a very long line that will wrap to multiple disp"
    assert!(
        content.ends_with("disp"),
        "Content should end with 'disp' after deletion"
    );
}

/// Test that line numbers are shown correctly with wrapped lines
#[test]
fn test_wrapped_line_numbers() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Create 3 logical lines, where the first one wraps
    harness.type_text("First line is very long and will wrap across multiple display rows in the terminal window.").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text("Second line is short.").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text("Third line is also short.").unwrap();

    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // Should show line numbers 1, 2, 3
    // The wrapped portions should show spaces in the line number area
    assert!(screen.contains("1"));
    assert!(screen.contains("2"));
    assert!(screen.contains("3"));

    // Verify we only have 3 logical lines in the buffer
    assert_eq!(
        harness.get_buffer_content().unwrap().matches('\n').count(),
        2
    );
}

/// Test that pressing End on a wrapped line goes to visual line end, no horizontal scroll
#[test]
fn test_wrapped_line_no_horizontal_scroll() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Type a long line that will wrap
    let long_text = "A fast, lightweight terminal text editor written in Rust. Handles files of any size with instant startup, low memory usage, and modern IDE features.";
    harness.type_text(long_text).unwrap();

    // Move cursor to document start (Ctrl+Home)
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let screen_before = harness.screen_to_string();

    // The line should be wrapped and visible from the beginning
    assert!(
        screen_before.contains("A fast"),
        "Should show start of line before End key"
    );
    assert!(
        screen_before.contains("lightweight"),
        "Should show 'lightweight' in wrapped portion"
    );

    // Press End - should go to end of VISUAL line (first segment), not physical line end
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let screen_after = harness.screen_to_string();

    // The screen should STILL show the beginning of the line (no horizontal scroll)
    // because End only moved to end of first visual segment
    assert!(screen_after.contains("A fast"),
            "After pressing End, line should still be wrapped and visible from start (no horizontal scroll). Screen:\n{screen_after}");

    // The cursor should NOT be at the physical line end - it should be at the end of the first visual segment
    assert!(
        harness.cursor_position() < long_text.len(),
        "Cursor should be at end of visual line segment, not end of physical line"
    );
}

/// Test cursor position updates correctly as it moves through wrapped lines
/// Verifies visual cursor moves down to wrapped portions and back up
#[test]
fn test_wrapped_line_cursor_positioning() {
    const TERMINAL_WIDTH: u16 = 60;

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, 24).unwrap();

    // Type a long line with real words that will wrap
    let long_text = "The quick brown fox jumps over the lazy dog and runs through the forest, exploring ancient trees and mysterious pathways that wind between towering oaks.";
    harness.type_text(long_text).unwrap();
    harness.render().unwrap();

    eprintln!("Text length: {}", long_text.len());

    // Cursor should be at end of text
    assert_eq!(harness.cursor_position(), long_text.len());

    // Move to start of line with Ctrl+Home (document start) to reliably reach position 0
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let (start_x, start_y) = harness.screen_cursor_position();
    assert_eq!(
        harness.cursor_position(),
        0,
        "Cursor should be at position 0 after Ctrl+Home"
    );

    // Cursor at position 0 should be at x=gutter_width (after gutter)
    let gutter_width = harness.editor().active_state().margins.left_total_width() as u16;
    assert_eq!(
        start_x, gutter_width,
        "Cursor at position 0 should be at x={gutter_width} (after gutter)"
    );

    // Verify the beginning of the text is visible on screen
    let screen = harness.screen_to_string();
    let text_start = &long_text[..20.min(long_text.len())]; // First 20 chars
    assert!(
        screen.contains(text_start),
        "Screen should show start of text: '{text_start}'"
    );
    eprintln!("Text start visible on screen: '{text_start}'");

    let mut prev_y = start_y;
    let mut first_wrap_point = None;
    let mut second_wrap_point = None;

    // Move right through the line to detect where wrapping occurs
    // We'll detect up to 2 wrap points to understand the wrapping pattern
    for i in 1..=long_text.len() {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
        harness.render().unwrap();

        let (cur_x, cur_y) = harness.screen_cursor_position();
        let buf_pos = harness.cursor_position();

        // Verify buffer position matches
        assert_eq!(buf_pos, i, "Buffer position should be {i}");

        // Detect when cursor wraps to next line
        if cur_y > prev_y {
            if first_wrap_point.is_none() {
                first_wrap_point = Some(i);
                eprintln!("After {i} rights: buffer_pos={buf_pos}, screen=({cur_x}, {cur_y}) -> FIRST WRAP");

                // At first wrap point, cursor should be at start of continuation line
                assert_eq!(
                    cur_x, gutter_width,
                    "At first wrap point (position {i}), cursor should be at x={gutter_width}"
                );
                assert_eq!(
                    cur_y,
                    start_y + 1,
                    "At first wrap point (position {i}), cursor should be on next line"
                );
                eprintln!("  ✓ First wrap point verified: position {i}, screen=({cur_x}, {cur_y})");
            } else if second_wrap_point.is_none() {
                second_wrap_point = Some(i);
                eprintln!("After {i} rights: buffer_pos={buf_pos}, screen=({cur_x}, {cur_y}) -> SECOND WRAP");

                // At second wrap point, cursor should also be at start of continuation line
                assert_eq!(
                    cur_x, gutter_width,
                    "At second wrap point (position {i}), cursor should be at x={gutter_width}"
                );
                assert_eq!(
                    cur_y,
                    start_y + 2,
                    "At second wrap point (position {i}), cursor should be two lines down"
                );
                eprintln!(
                    "  ✓ Second wrap point verified: position {i}, screen=({cur_x}, {cur_y})"
                );

                // We've detected both wrap points, we can break
                break;
            }
        }

        prev_y = cur_y;
    }

    assert!(
        first_wrap_point.is_some(),
        "Should have detected first wrap point"
    );
    assert!(
        second_wrap_point.is_some(),
        "Should have detected second wrap point"
    );

    let first_line_width = first_wrap_point.unwrap();
    let continuation_line_width = second_wrap_point.unwrap() - first_wrap_point.unwrap();
    eprintln!("Detected wrapping: first_line_width={first_line_width}, continuation_line_width={continuation_line_width}");

    // Verify no horizontal scroll happened throughout
    let screen = harness.screen_to_string();
    assert!(
        screen.contains(text_start),
        "Screen should still show start of text (no horizontal scroll)"
    );

    // Now press End to jump to end of current visual segment
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let end_pos_after_end = harness.cursor_position();
    let (_end_x, _end_y) = harness.screen_cursor_position();

    // End key now goes to visual line end, not physical line end
    // Keep pressing End until we reach physical line end
    let mut pos = end_pos_after_end;
    while pos < long_text.len() {
        harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
        harness.render().unwrap();
        let new_pos = harness.cursor_position();
        assert!(
            new_pos > pos,
            "End key should make progress toward line end"
        );
        pos = new_pos;
    }

    let (_end_x, end_y) = harness.screen_cursor_position();
    assert_eq!(
        harness.cursor_position(),
        long_text.len(),
        "Cursor should be at end after repeated End keys"
    );
    eprintln!(
        "After End(s): buffer_pos={}, screen_y={}",
        long_text.len(),
        end_y,
    );

    // Verify cursor ended up on a later line (text wrapped at least once)
    assert!(
        end_y > start_y,
        "End cursor should be on a later line than start (text should wrap)"
    );

    // Verify text is visible on screen
    let screen_at_end = harness.screen_to_string();
    assert!(
        screen_at_end.contains("The quick brown fox"),
        "Screen should show beginning of text"
    );
    // Just verify some text from the end is visible (exact text depends on wrapping)
    assert!(
        screen_at_end.contains("oaks")
            || screen_at_end.contains("tower")
            || screen_at_end.contains("between"),
        "Screen should show some text from end of line"
    );

    // Now move back left and watch cursor move back up across wrap points
    let mut wrapped_up = false;
    let mut prev_y = end_y;

    // Move left through the text, watching for upward wrapping.
    // Loop bound must exceed the visual line width so we reliably cross at
    // least one wrap boundary from the physical end of the text.
    for i in 1..=long_text.len() {
        harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
        harness.render().unwrap();

        let (cur_x, cur_y) = harness.screen_cursor_position();
        let buf_pos = harness.cursor_position();

        // Check if cursor wrapped back up
        if cur_y < prev_y {
            eprintln!(
                "After {i} lefts: buffer_pos={buf_pos}, screen=({cur_x}, {cur_y}) -> WRAPPED UP"
            );
            wrapped_up = true;

            // When wrapping up, cursor should NOT be at gutter (should be at end of previous line)
            assert!(cur_x > gutter_width, "When wrapping up, cursor should be at end of previous line, not at x={gutter_width}");

            // We've verified upward wrapping works
            break;
        }

        prev_y = cur_y;
    }

    assert!(
        wrapped_up,
        "Cursor should have wrapped back up when moving left across wrap boundaries"
    );

    // Finally, press Ctrl+Home to go back to document start (position 0)
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let (final_x, final_y) = harness.screen_cursor_position();
    assert_eq!(
        harness.cursor_position(),
        0,
        "Cursor should be at position 0 after Ctrl+Home"
    );
    assert_eq!(
        final_x, gutter_width,
        "Cursor should be at x={gutter_width}"
    );
    assert_eq!(final_y, start_y, "Cursor should be back at starting y");

    // Verify start of text is still visible
    let screen_final = harness.screen_to_string();
    assert!(
        screen_final.contains(text_start),
        "Start of text should still be visible after Home"
    );
}

/// Test that scrolling works correctly when navigating down past viewport with wrapped lines
/// Bug: Using down arrow to scroll past the end of the view area doesn't scroll the page correctly
/// This test validates that the cursor's buffer position always corresponds to visible content
#[test]
fn test_wrapped_line_scrolling_down_past_viewport() {
    const TERMINAL_WIDTH: u16 = 60;
    const TERMINAL_HEIGHT: u16 = 12; // Small height to make scrolling happen quickly

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, TERMINAL_HEIGHT).unwrap();

    // Create multiple long lines with identifiable prefixes
    // Each line starts with "[N]" so we can identify which line the cursor is on
    // Lines are long enough to wrap to 2-3 screen lines
    for i in 0..20 {
        harness.type_text(&format!(
            "[{}] This is line number {} with lots of extra text to make it wrap across multiple display rows in the terminal window. ",
            i, i
        )).unwrap();
        if i < 19 {
            harness
                .send_key(KeyCode::Enter, KeyModifiers::NONE)
                .unwrap();
        }
    }

    // Move cursor to the beginning
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let buffer_content = harness.get_buffer_content().unwrap();
    eprintln!("\n=== Buffer content ===");
    eprintln!("Total buffer length: {} bytes", buffer_content.len());

    let (initial_x, initial_y) = harness.screen_cursor_position();
    eprintln!("\n=== Initial state ===");
    eprintln!("Initial cursor position: ({}, {})", initial_x, initial_y);

    // Verify we're at the start
    assert_eq!(harness.cursor_position(), 0, "Should be at start of buffer");

    // Get the initial screen content to see what's visible
    let screen_before = harness.screen_to_string();
    eprintln!("Screen before scrolling:\n{}", screen_before);

    // Helper function to determine which line number the cursor is on based on buffer position
    let get_line_at_position = |pos: usize| -> Option<usize> {
        let text_up_to_cursor = &buffer_content[..pos.min(buffer_content.len())];
        let lines_before = text_up_to_cursor.matches('\n').count();
        Some(lines_before)
    };

    // Press down repeatedly to move through wrapped lines
    let mut scrolling_occurred = false;
    let max_down_presses = 50;

    for i in 0..max_down_presses {
        harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
        harness.render().unwrap();

        let (cur_x, cur_y) = harness.screen_cursor_position();
        let buf_pos = harness.cursor_position();

        // Get the screen content
        let screen_now = harness.screen_to_string();

        // Determine which line the cursor is on
        let cursor_line = get_line_at_position(buf_pos).unwrap();

        eprintln!("\n=== After {} down presses ===", i + 1);
        eprintln!("  Buffer position: {}", buf_pos);
        eprintln!("  Screen position: ({}, {})", cur_x, cur_y);
        eprintln!("  Cursor is on logical line: {}", cursor_line);

        // The cursor should ALWAYS be visible on screen
        assert!(
            cur_y < TERMINAL_HEIGHT,
            "After {} down presses: Cursor at y={} is beyond terminal height {}. \
             Buffer position: {}. This indicates scrolling didn't happen when it should have.",
            i + 1,
            cur_y,
            TERMINAL_HEIGHT,
            buf_pos
        );

        // CRITICAL CHECK: The line number the cursor is on should be visible in the viewport
        // Look for the line identifier "[N]" where N is the cursor's line number
        let line_marker = format!("[{}]", cursor_line);
        assert!(
            screen_now.contains(&line_marker),
            "After {} down presses: Cursor is on line {} (position {}), but '{}' is NOT visible on screen!\n\
             This means scrolling did not happen correctly.\n\
             Screen content:\n{}",
            i + 1,
            cursor_line,
            buf_pos,
            line_marker,
            screen_now
        );

        eprintln!("  ✓ Line marker '{}' is visible on screen", line_marker);

        // Additional validation: check a few characters around the cursor position
        if buf_pos > 0 && buf_pos < buffer_content.len() {
            let start = buf_pos.saturating_sub(5);
            let end = (buf_pos + 5).min(buffer_content.len());
            let context = &buffer_content[start..end];
            let context_clean = context.replace('\n', "\\n");

            eprintln!("  Context around cursor: '...{}...'", context_clean);

            // Check if any part of this context is visible on screen
            // (accounting for line breaks)
            for word in context.split_whitespace() {
                if word.len() >= 3 && screen_now.contains(word) {
                    eprintln!("  ✓ Found context word '{}' on screen", word);
                    break;
                }
            }
        }

        // Check if scrolling has occurred
        if !screen_now.contains("[0]") && !scrolling_occurred {
            eprintln!("\n=== SCROLLING DETECTED after {} down presses ===", i + 1);
            scrolling_occurred = true;
        }

        // Stop after we've scrolled significantly
        if cursor_line >= 12 {
            eprintln!("\n=== Reached line {}, stopping test ===", cursor_line);
            break;
        }
    }

    assert!(
        scrolling_occurred,
        "Scrolling should have occurred when navigating through wrapped lines"
    );

    // Final validation
    let final_pos = harness.cursor_position();
    let final_line = get_line_at_position(final_pos).unwrap();
    eprintln!("\n=== Final state ===");
    eprintln!("  Final buffer position: {}", final_pos);
    eprintln!("  Final line: {}", final_line);

    assert!(
        final_line >= 5,
        "Should have navigated to at least line 5, but only reached line {}",
        final_line
    );
}

/// Test mouse clicking on wrapped lines positions cursor correctly
/// This test validates that clicking on:
/// 1. The first visual row of a wrapped line
/// 2. Continuation rows (wrapped portions)
/// 3. Empty lines
///    all position the cursor at the correct buffer offset
#[test]
fn test_mouse_click_on_wrapped_lines() {
    // Initialize tracing for debugging
    let _ = tracing_subscriber::fmt()
        .with_env_filter("fresh=debug")
        .with_test_writer()
        .try_init();

    const TERMINAL_WIDTH: u16 = 60;
    const TERMINAL_HEIGHT: u16 = 24;
    const GUTTER_WIDTH: u16 = 8; // Line numbers + margin

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, TERMINAL_HEIGHT).unwrap();

    // Create content with:
    // Line 1: A long line that will wrap to multiple visual rows
    // Line 2: An empty line
    // Line 3: A short line
    let long_line =
        "The quick brown fox jumps over the lazy dog and continues running through the forest.";
    let short_line = "Short line here.";

    harness.type_text(long_line).unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    // Empty line (just press Enter again)
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text(short_line).unwrap();

    harness.render().unwrap();

    let buffer_content = harness.get_buffer_content().unwrap();
    eprintln!("Buffer content ({} bytes):", buffer_content.len());
    eprintln!(
        "  Line 1 (long): '{}' ({} chars)",
        long_line,
        long_line.len()
    );
    eprintln!("  Line 2: (empty)");
    eprintln!(
        "  Line 3 (short): '{}' ({} chars)",
        short_line,
        short_line.len()
    );

    // Calculate expected positions
    let line1_start = 0usize;
    let line2_start = long_line.len() + 1; // after long_line + newline
    let line3_start = line2_start + 1; // after empty line's newline

    eprintln!("\nExpected buffer positions:");
    eprintln!("  Line 1 starts at byte: {}", line1_start);
    eprintln!("  Line 2 starts at byte: {}", line2_start);
    eprintln!("  Line 3 starts at byte: {}", line3_start);

    // Get content area info
    let (content_first_row, _content_last_row) = harness.content_area_rows();
    eprintln!("\nContent area starts at row: {}", content_first_row);

    // ========================================
    // Test 1: Click on first visual row of wrapped line (line 1)
    // ========================================
    eprintln!("\n=== Test 1: Click on first row of wrapped line ===");

    // Click near the beginning of the first line (in text area, after gutter)
    let click_x = GUTTER_WIDTH + 5; // 5 chars into the text
    let click_y = content_first_row as u16;

    harness.mouse_click(click_x, click_y).unwrap();
    harness.render().unwrap();

    let pos_after_click1 = harness.cursor_position();
    eprintln!(
        "Clicked at screen ({}, {}), cursor now at buffer position: {}",
        click_x, click_y, pos_after_click1
    );

    // Cursor should be near position 5 (within the first line)
    assert!(
        pos_after_click1 < long_line.len(),
        "Click on first row should position cursor within line 1 (pos {} should be < {})",
        pos_after_click1,
        long_line.len()
    );
    // Should be roughly where we clicked (allowing some tolerance for character width)
    assert!(
        (3..=10).contains(&pos_after_click1),
        "Click at x={} should position cursor around position 5, got {}",
        click_x,
        pos_after_click1
    );
    eprintln!("  ✓ Cursor correctly positioned in first visual row");

    // Wait to avoid double-click detection (use config value * 2 for safety margin)
    let double_click_delay =
        std::time::Duration::from_millis(harness.config().editor.double_click_time_ms * 2);
    std::thread::sleep(double_click_delay);

    // ========================================
    // Test 2: Click on continuation row (second visual row of line 1)
    // ========================================
    eprintln!("\n=== Test 2: Click on wrapped continuation row ===");

    // The text width available is TERMINAL_WIDTH - GUTTER_WIDTH - 1 (scrollbar) = 60 - 8 - 1 = 51
    // So the first wrap should occur around character 51
    let text_width = (TERMINAL_WIDTH - GUTTER_WIDTH - 1) as usize;
    eprintln!("Text width per row: {} chars", text_width);

    // Click on the second visual row (continuation of line 1)
    let click_x = GUTTER_WIDTH + 10; // 10 chars into the continuation
    let click_y = content_first_row as u16 + 1; // Second visual row

    harness.mouse_click(click_x, click_y).unwrap();
    harness.render().unwrap();

    let pos_after_click2 = harness.cursor_position();
    eprintln!(
        "Clicked at screen ({}, {}), cursor now at buffer position: {}",
        click_x, click_y, pos_after_click2
    );

    // Cursor should be in the wrapped portion of line 1
    // That means position should be >= text_width (past first visual row)
    // and still within line 1 (< long_line.len())
    assert!(
        pos_after_click2 >= text_width.saturating_sub(5),
        "Click on continuation row should position cursor past first visual row (pos {} should be >= ~{})",
        pos_after_click2,
        text_width
    );
    assert!(
        pos_after_click2 < long_line.len(),
        "Click on continuation row should stay within line 1 (pos {} should be < {})",
        pos_after_click2,
        long_line.len()
    );
    eprintln!("  ✓ Cursor correctly positioned in continuation row");

    // Wait to avoid double-click detection
    std::thread::sleep(double_click_delay);

    // ========================================
    // Test 3: Click on empty line (line 2)
    // ========================================
    eprintln!("\n=== Test 3: Click on empty line ===");

    // First, find which visual row the empty line is on
    // Line 1 wraps to ~2 visual rows (85 chars / 51 chars per row ≈ 2 rows)
    let visual_rows_for_line1 = long_line.len().div_ceil(text_width);
    eprintln!("Line 1 takes {} visual rows", visual_rows_for_line1);

    let empty_line_visual_row = content_first_row + visual_rows_for_line1;
    eprintln!(
        "Empty line should be at visual row: {}",
        empty_line_visual_row
    );

    // Click on the empty line
    let click_x = GUTTER_WIDTH + 5; // Doesn't matter much for empty line
    let click_y = empty_line_visual_row as u16;

    harness.mouse_click(click_x, click_y).unwrap();
    harness.render().unwrap();

    let pos_after_click3 = harness.cursor_position();
    eprintln!(
        "Clicked at screen ({}, {}), cursor now at buffer position: {}",
        click_x, click_y, pos_after_click3
    );

    // Cursor should be at the start of line 2 (the empty line)
    // or at the newline position of line 1
    assert!(
        pos_after_click3 >= long_line.len() && pos_after_click3 <= line2_start,
        "Click on empty line should position cursor at/near line 2 start (pos {} should be around {})",
        pos_after_click3,
        line2_start
    );
    eprintln!("  ✓ Cursor correctly positioned on empty line");

    // Wait to avoid double-click detection
    std::thread::sleep(double_click_delay);

    // ========================================
    // Test 4: Click on line after empty line (line 3)
    // ========================================
    eprintln!("\n=== Test 4: Click on line after empty line ===");

    let short_line_visual_row = empty_line_visual_row + 1;
    eprintln!(
        "Short line should be at visual row: {}",
        short_line_visual_row
    );

    // Click on the short line
    let click_x = GUTTER_WIDTH + 3;
    let click_y = short_line_visual_row as u16;

    harness.mouse_click(click_x, click_y).unwrap();
    harness.render().unwrap();

    let pos_after_click4 = harness.cursor_position();
    eprintln!(
        "Clicked at screen ({}, {}), cursor now at buffer position: {}",
        click_x, click_y, pos_after_click4
    );

    // Cursor should be within line 3
    assert!(
        pos_after_click4 >= line3_start,
        "Click on line 3 should position cursor at or after line 3 start (pos {} should be >= {})",
        pos_after_click4,
        line3_start
    );
    assert!(
        pos_after_click4 <= line3_start + short_line.len(),
        "Click on line 3 should position cursor within line 3 (pos {} should be <= {})",
        pos_after_click4,
        line3_start + short_line.len()
    );
    eprintln!("  ✓ Cursor correctly positioned on line after empty line");

    // Wait to avoid double-click detection
    std::thread::sleep(double_click_delay);

    // ========================================
    // Test 5: Click at end of wrapped line (rightmost position before wrap)
    // ========================================
    eprintln!("\n=== Test 5: Click at end of first visual row ===");

    // Click at the rightmost text position of the first visual row
    let click_x = TERMINAL_WIDTH - 2; // Just before the scrollbar
    let click_y = content_first_row as u16;

    harness.mouse_click(click_x, click_y).unwrap();
    harness.render().unwrap();

    let pos_after_click5 = harness.cursor_position();
    eprintln!(
        "Clicked at screen ({}, {}), cursor now at buffer position: {}",
        click_x, click_y, pos_after_click5
    );

    // Cursor should be near the end of the first visual row
    // (around text_width position, give or take)
    assert!(
        pos_after_click5 >= text_width.saturating_sub(5) && pos_after_click5 <= text_width + 5,
        "Click at end of first visual row should position cursor near wrap point (pos {} should be around {})",
        pos_after_click5,
        text_width
    );
    eprintln!("  ✓ Cursor correctly positioned at end of first visual row");

    eprintln!("\n=== All mouse click tests passed! ===");
}

/// Test that cursor doesn't move into empty space beyond wrapped line ends
/// Bug: Cursor can move several characters past the visible text before wrapping down
/// TODO: This test is currently disabled due to rendering issues that need investigation
#[test]
#[ignore]
fn test_wrapped_line_cursor_no_empty_space() {
    const TERMINAL_WIDTH: u16 = 60;

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, 24).unwrap();

    // Type a line that will wrap
    let long_text = "The quick brown fox jumps over the lazy dog and runs through the forest.";
    harness.type_text(long_text).unwrap();

    // Move to document start (Ctrl+Home) to reliably get to position 0
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let (_start_x, start_y) = harness.screen_cursor_position();
    eprintln!("\n=== Testing cursor doesn't go into empty space ===");
    eprintln!("Text: '{long_text}'");
    eprintln!("Length: {} chars", long_text.len());

    // Move right character by character and check what's under the cursor
    let mut found_empty_space = false;
    let mut wrap_happened = false;
    let mut prev_y = start_y;

    for i in 0..long_text.len() {
        let (cur_x, cur_y) = harness.screen_cursor_position();
        let cell_content = harness.get_cell(cur_x, cur_y);
        let buf_pos = harness.cursor_position();

        eprintln!(
            "Position {i}: screen=({cur_x}, {cur_y}), buffer={buf_pos}, cell='{cell_content:?}'"
        );

        // Check if cursor wrapped to next line
        if cur_y > prev_y {
            eprintln!("  -> Cursor wrapped from y={prev_y} to y={cur_y}");
            wrap_happened = true;
        }
        prev_y = cur_y;

        // Verify cursor is on the expected character from the text
        let expected_char = long_text.chars().nth(i).unwrap();
        let is_space_in_text = expected_char == ' ';

        if let Some(content) = &cell_content {
            // Check if this is actual content from the text or padding
            if !is_space_in_text && content == " " {
                // The text character is not a space, but we're seeing a space on screen
                // This means we're in empty padding beyond the wrapped line
                eprintln!("  ⚠️  CURSOR IN EMPTY SPACE at screen position ({cur_x}, {cur_y})");
                eprintln!(
                    "      Expected char '{expected_char}' from text, but screen shows space"
                );
                found_empty_space = true;
            }
            // Also check if cursor is on scrollbar (rendered with background colors)
            if harness.is_scrollbar_thumb_at(cur_x, cur_y)
                || harness.is_scrollbar_track_at(cur_x, cur_y)
            {
                // Hit scrollbar or UI element
                eprintln!("  ⚠️  CURSOR ON UI ELEMENT (scrollbar?) at ({cur_x}, {cur_y})");
                found_empty_space = true;
            }
        }

        // Move right
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
        harness.render().unwrap();
    }

    assert!(
        !found_empty_space,
        "Cursor should never move into empty space beyond visible text"
    );
    assert!(wrap_happened, "Cursor should have wrapped to next line");

    // Now test pressing End from the start
    harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let (end_x, end_y) = harness.screen_cursor_position();
    let buf_pos_at_end = harness.cursor_position();

    eprintln!("\n=== After pressing End ===");
    eprintln!("Cursor at: screen=({end_x}, {end_y}), buffer={buf_pos_at_end}");

    // The cursor should be at the actual end of the text
    assert_eq!(
        buf_pos_at_end,
        long_text.len(),
        "End key should move to end of buffer"
    );

    // Check what's under and around the cursor
    let cell_at_cursor = harness.get_cell(end_x, end_y);
    let cell_before_cursor = if end_x > 0 {
        harness.get_cell(end_x - 1, end_y)
    } else {
        None
    };

    eprintln!("Cell at cursor ({end_x}, {end_y}): {cell_at_cursor:?}");
    eprintln!(
        "Cell before cursor ({}, {}): {:?}",
        end_x - 1,
        end_y,
        cell_before_cursor
    );

    // The cell before the cursor should have actual text content (the last character)
    // The cursor itself might be on empty space (end of line) or the last character
    if let Some(before) = cell_before_cursor {
        assert!(
            !before.trim().is_empty() || before == " ",
            "Cell before cursor should have text content, found: '{before}'"
        );
    }
}

/// Test that every character of a wrapped line is visible in the viewport
/// This ensures no characters are lost or hidden at wrap boundaries
#[test]
fn test_wrapped_line_all_characters_visible() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Use unique words (numbers) so we can verify each one appears in the output
    // This text will wrap multiple times in a 60-column terminal (with ~8 col gutter = ~52 usable)
    let words: Vec<String> = (10000..10025).map(|n| n.to_string()).collect();
    let text = words.join(" ");
    harness.type_text(&text).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // Verify every unique word appears in the screen
    for word in &words {
        assert!(
            screen.contains(word),
            "Word '{}' is missing from screen output.\nText: {}\nScreen:\n{}",
            word,
            text,
            screen
        );
    }
}

/// Test that wrapped lines with leading tabs have all characters visible
/// Tabs at the beginning take up visual space but shouldn't hide content
#[test]
fn test_wrapped_line_with_tabs_all_characters_visible() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Use unique words (numbers) with leading tabs
    // Tabs reduce available space for content, causing more wrapping
    let words: Vec<String> = (20000..20020).map(|n| n.to_string()).collect();
    let text = format!("\t\t{}", words.join(" "));
    harness.type_text(&text).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // Verify every unique word appears in the screen
    for word in &words {
        assert!(
            screen.contains(word),
            "Word '{}' is missing from screen output.\nText: {}\nScreen:\n{}",
            word,
            text,
            screen
        );
    }
}

/// Test that line numbers in the gutter match the correct line content when scrolling with wrapped lines
/// Reproduces issue #552: line numbers get out of sync when scrolling through CSV files with long lines
/// https://github.com/sinelaw/fresh/issues/552
#[test]
fn test_line_numbers_correct_with_wrapping_and_scrolling() {
    use std::io::Write;

    // Use dimensions similar to the bug report - narrower terminal causes more wrapping
    const TERMINAL_WIDTH: u16 = 82;
    const TERMINAL_HEIGHT: u16 = 20;

    // Create CSV-like data similar to issue #552
    // Each line starts with its row number, making it easy to verify line numbers match content
    let csv_content = "\
1,Wii Sports,Wii,2006,Sports,Nintendo,41.49,29.02,3.77,8.46,82.74
2,Super Mario Bros.,NES,1985,Platform,Nintendo,29.08,3.58,6.81,0.77,40.24
3,Mario Kart Wii,Wii,2008,Racing,Nintendo,15.85,12.88,3.79,3.31,35.82
4,Wii Sports Resort,Wii,2009,Sports,Nintendo,15.75,11.01,3.28,2.96,33
5,Pokemon Red/Pokemon Blue,GB,1996,Role-Playing,Nintendo,11.27,8.89,10.22,1,31.37
6,Tetris,GB,1989,Puzzle,Nintendo,23.2,2.26,4.22,0.58,30.26
7,New Super Mario Bros.,DS,2006,Platform,Nintendo,11.38,9.23,6.5,2.9,30.01
8,Wii Play,Wii,2006,Misc,Nintendo,14.03,9.2,2.93,2.85,29.02
9,New Super Mario Bros. Wii,Wii,2009,Platform,Nintendo,14.59,7.06,4.7,2.26,28.62
10,Duck Hunt,NES,1984,Shooter,Nintendo,26.93,0.63,0.28,0.47,28.31
11,Nintendogs,DS,2005,Simulation,Nintendo,9.07,11,1.93,2.75,24.76
12,Mario Kart DS,DS,2005,Racing,Nintendo,9.81,7.57,4.13,1.92,23.42
13,Pokemon Gold/Pokemon Silver,GB,1999,Role-Playing,Nintendo,9,6.18,7.2,0.71,23.1
14,Wii Fit,Wii,2007,Sports,Nintendo,8.94,8.03,3.6,2.15,22.72
15,Wii Fit Plus,Wii,2009,Sports,Nintendo,9.09,8.59,2.53,1.79,22
16,Kinect Adventures!,X360,2010,Misc,Microsoft Game Studios,14.97,4.94,0.24,1.67,21.82
17,Grand Theft Auto V,PS3,2013,Action,Take-Two Interactive,7.01,9.27,0.97,4.14,21.4
18,Grand Theft Auto: San Andreas,PS2,2004,Action,Take-Two Interactive,9.43,0.4,0.41,10.57,20.81
19,Super Mario World,SNES,1990,Platform,Nintendo,12.78,3.75,3.54,0.55,20.61
20,Brain Age: Train Your Brain in Minutes a Day,DS,2005,Misc,Nintendo,4.75,9.26,4.16,2.05,20.22
";

    // Write to temp file and load it (to match issue #552 reproduction)
    let temp_dir = tempfile::TempDir::new().unwrap();
    let csv_path = temp_dir.path().join("vgsales.csv");
    let mut file = std::fs::File::create(&csv_path).unwrap();
    file.write_all(csv_content.as_bytes()).unwrap();

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, TERMINAL_HEIGHT).unwrap();

    // Open the file (like the user in issue #552)
    harness.open_file(&csv_path).unwrap();

    // Helper function to verify line numbers match content
    // For CSV data, each line starts with its row number (1,... 2,... etc.)
    // The gutter line number should match the row number at the start of content
    fn verify_line_numbers_match_content(screen: &str, context: &str) {
        for screen_line in screen.lines() {
            // Skip non-content lines (menu bar, status bar, empty lines)
            if !screen_line.contains("│") {
                continue;
            }

            if let Some(bar_pos) = screen_line.find('│') {
                let gutter = &screen_line[..bar_pos];
                let content = &screen_line[bar_pos + "│".len()..]; // Skip the bar character

                // Extract the line number from gutter
                let gutter_line_num: Option<u32> = gutter.trim().parse().ok();

                if let Some(gutter_num) = gutter_line_num {
                    // This row has a line number in the gutter
                    // The content should start with that same number followed by comma
                    let content_trimmed = content.trim_start();

                    // Extract the first number from content (CSV row number)
                    let content_num: Option<u32> = content_trimmed
                        .split(',')
                        .next()
                        .and_then(|s| s.trim().parse().ok());

                    if let Some(csv_num) = content_num {
                        assert_eq!(
                            gutter_num, csv_num,
                            "{}: Gutter shows line {} but content starts with row {}\nRow: '{}'\nFull screen:\n{}",
                            context,
                            gutter_num,
                            csv_num,
                            screen_line,
                            screen
                        );
                    }
                }
            }
        }
    }

    // Move to beginning of document
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let screen_at_top = harness.screen_to_string();
    eprintln!("=== Screen at top ===\n{}", screen_at_top);

    // Verify line numbers match content at top
    verify_line_numbers_match_content(&screen_at_top, "At top of document");

    // Test 1: Scroll down one line at a time and verify
    for i in 1..=15 {
        harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
        harness.render().unwrap();

        let screen = harness.screen_to_string();
        eprintln!("=== After {} down presses ===\n{}", i, screen);
        verify_line_numbers_match_content(&screen, &format!("After {} down presses", i));
    }

    // Test 2: Scroll back up and verify - this is key to reproducing #552
    for i in 1..=15 {
        harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
        harness.render().unwrap();

        let screen = harness.screen_to_string();
        eprintln!("=== After {} up presses ===\n{}", i, screen);
        verify_line_numbers_match_content(&screen, &format!("After {} up presses", i));
    }

    // Test 3: Use PageDown multiple times to scroll more aggressively
    // This is the key reproduction for issue #552
    for i in 1..=5 {
        harness
            .send_key(KeyCode::PageDown, KeyModifiers::NONE)
            .unwrap();
        harness.render().unwrap();
        let screen = harness.screen_to_string();
        eprintln!("=== After PageDown {} ===\n{}", i, screen);
        verify_line_numbers_match_content(&screen, &format!("After PageDown {}", i));
    }

    // Test 4: Use PageUp multiple times
    for i in 1..=5 {
        harness
            .send_key(KeyCode::PageUp, KeyModifiers::NONE)
            .unwrap();
        harness.render().unwrap();
        let screen = harness.screen_to_string();
        eprintln!("=== After PageUp {} ===\n{}", i, screen);
        verify_line_numbers_match_content(&screen, &format!("After PageUp {}", i));
    }

    // Test 5: Go to end of file and then scroll up
    harness
        .send_key(KeyCode::End, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    let screen = harness.screen_to_string();
    eprintln!("=== After Ctrl+End ===\n{}", screen);
    verify_line_numbers_match_content(&screen, "After Ctrl+End");

    // Scroll up from end
    for i in 1..=10 {
        harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
        harness.render().unwrap();

        let screen = harness.screen_to_string();
        eprintln!("=== From end: after {} up presses ===\n{}", i, screen);
        verify_line_numbers_match_content(&screen, &format!("From end: {} up presses", i));
    }

    // Test 6: Go to start and scroll down again
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();
    let screen = harness.screen_to_string();
    eprintln!("=== After Ctrl+Home ===\n{}", screen);
    verify_line_numbers_match_content(&screen, "After Ctrl+Home");

    // Final scroll down cycle
    for i in 1..=10 {
        harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
        harness.render().unwrap();

        let screen = harness.screen_to_string();
        eprintln!("=== Final cycle: after {} down presses ===\n{}", i, screen);
        verify_line_numbers_match_content(&screen, &format!("Final down cycle {}", i));
    }
}

/// Test that line numbers remain correct when using PageDown to scroll through wrapped lines
/// Reproduces issue #552: line numbers desync when using PageDown in CSV files
#[test]
fn test_line_numbers_with_pagedown_scrolling() {
    use std::io::Write;

    const TERMINAL_WIDTH: u16 = 82;
    const TERMINAL_HEIGHT: u16 = 20;

    // Create CSV data where each line starts with its row number
    let csv_content = "\
1,Wii Sports,Wii,2006,Sports,Nintendo,41.49,29.02,3.77,8.46,82.74
2,Super Mario Bros.,NES,1985,Platform,Nintendo,29.08,3.58,6.81,0.77,40.24
3,Mario Kart Wii,Wii,2008,Racing,Nintendo,15.85,12.88,3.79,3.31,35.82
4,Wii Sports Resort,Wii,2009,Sports,Nintendo,15.75,11.01,3.28,2.96,33
5,Pokemon Red/Pokemon Blue,GB,1996,Role-Playing,Nintendo,11.27,8.89,10.22,1,31.37
6,Tetris,GB,1989,Puzzle,Nintendo,23.2,2.26,4.22,0.58,30.26
7,New Super Mario Bros.,DS,2006,Platform,Nintendo,11.38,9.23,6.5,2.9,30.01
8,Wii Play,Wii,2006,Misc,Nintendo,14.03,9.2,2.93,2.85,29.02
9,New Super Mario Bros. Wii,Wii,2009,Platform,Nintendo,14.59,7.06,4.7,2.26,28.62
10,Duck Hunt,NES,1984,Shooter,Nintendo,26.93,0.63,0.28,0.47,28.31
11,Nintendogs,DS,2005,Simulation,Nintendo,9.07,11,1.93,2.75,24.76
12,Mario Kart DS,DS,2005,Racing,Nintendo,9.81,7.57,4.13,1.92,23.42
13,Pokemon Gold/Pokemon Silver,GB,1999,Role-Playing,Nintendo,9,6.18,7.2,0.71,23.1
14,Wii Fit,Wii,2007,Sports,Nintendo,8.94,8.03,3.6,2.15,22.72
15,Wii Fit Plus,Wii,2009,Sports,Nintendo,9.09,8.59,2.53,1.79,22
16,Kinect Adventures!,X360,2010,Misc,Microsoft Game Studios,14.97,4.94,0.24,1.67,21.82
17,Grand Theft Auto V,PS3,2013,Action,Take-Two Interactive,7.01,9.27,0.97,4.14,21.4
18,Grand Theft Auto: San Andreas,PS2,2004,Action,Take-Two Interactive,9.43,0.4,0.41,10.57,20.81
19,Super Mario World,SNES,1990,Platform,Nintendo,12.78,3.75,3.54,0.55,20.61
20,Brain Age: Train Your Brain in Minutes a Day,DS,2005,Misc,Nintendo,4.75,9.26,4.16,2.05,20.22
21,Pokemon Diamond/Pokemon Pearl,DS,2006,Role-Playing,Nintendo,6.42,4.52,6.04,1.37,18.36
22,Super Mario Land,GB,1989,Platform,Nintendo,10.83,2.71,4.18,0.42,18.14
23,Super Mario Bros. 3,NES,1988,Platform,Nintendo,9.54,3.44,3.84,0.46,17.28
24,Grand Theft Auto V,X360,2013,Action,Take-Two Interactive,9.63,5.31,0.06,1.38,16.38
25,Grand Theft Auto: Vice City,PS2,2002,Action,Take-Two Interactive,8.41,5.49,0.47,1.78,16.15
";

    // Write to temp file
    let temp_dir = tempfile::TempDir::new().unwrap();
    let csv_path = temp_dir.path().join("vgsales.csv");
    let mut file = std::fs::File::create(&csv_path).unwrap();
    file.write_all(csv_content.as_bytes()).unwrap();

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, TERMINAL_HEIGHT).unwrap();
    harness.open_file(&csv_path).unwrap();

    // Helper function to verify line numbers match content
    fn verify_line_numbers_match_content(screen: &str, context: &str) {
        for screen_line in screen.lines() {
            if !screen_line.contains("│") {
                continue;
            }

            if let Some(bar_pos) = screen_line.find('│') {
                let gutter = &screen_line[..bar_pos];
                let content = &screen_line[bar_pos + "│".len()..];
                let gutter_line_num: Option<u32> = gutter.trim().parse().ok();

                if let Some(gutter_num) = gutter_line_num {
                    let content_trimmed = content.trim_start();
                    let content_num: Option<u32> = content_trimmed
                        .split(',')
                        .next()
                        .and_then(|s| s.trim().parse().ok());

                    if let Some(csv_num) = content_num {
                        assert_eq!(
                            gutter_num, csv_num,
                            "{}: Gutter shows line {} but content starts with row {}\nRow: '{}'\nFull screen:\n{}",
                            context, gutter_num, csv_num, screen_line, screen
                        );
                    }
                }
            }
        }
    }

    harness.render().unwrap();
    let screen = harness.screen_to_string();
    eprintln!("=== Initial screen ===\n{}", screen);
    verify_line_numbers_match_content(&screen, "Initial");

    // Press PageDown multiple times - this is the key reproduction for issue #552
    for i in 1..=5 {
        harness
            .send_key(KeyCode::PageDown, KeyModifiers::NONE)
            .unwrap();
        harness.render().unwrap();

        let screen = harness.screen_to_string();
        eprintln!("=== After PageDown {} ===\n{}", i, screen);
        verify_line_numbers_match_content(&screen, &format!("After PageDown {}", i));
    }

    // Press PageUp to go back
    for i in 1..=5 {
        harness
            .send_key(KeyCode::PageUp, KeyModifiers::NONE)
            .unwrap();
        harness.render().unwrap();

        let screen = harness.screen_to_string();
        eprintln!("=== After PageUp {} ===\n{}", i, screen);
        verify_line_numbers_match_content(&screen, &format!("After PageUp {}", i));
    }
}

/// Test that line numbers remain correct with a single PageDown in a narrow terminal
/// Reproduces issue #552: the root cause is top_view_line_offset becoming stale when top_byte is updated
/// The bug manifests when scrolling causes the viewport to jump too far due to stale offset
#[test]
fn test_line_numbers_single_pagedown_narrow_terminal() {
    use std::io::Write;

    // Use a very narrow terminal to force aggressive line wrapping
    const TERMINAL_WIDTH: u16 = 50;
    const TERMINAL_HEIGHT: u16 = 18;

    // Create CSV data where each line starts with its row number
    // Lines are designed to wrap at 50 chars
    let csv_content = "\
1,Wii Sports,Wii,2006,Sports,Nintendo,41.49,29.02,3.77,8.46,82.74
2,Super Mario Bros.,NES,1985,Platform,Nintendo,29.08,3.58,6.81,0.77,40.24
3,Mario Kart Wii,Wii,2008,Racing,Nintendo,15.85,12.88,3.79,3.31,35.82
4,Wii Sports Resort,Wii,2009,Sports,Nintendo,15.75,11.01,3.28,2.96,33
5,Pokemon Red/Pokemon Blue,GB,1996,Role-Playing,Nintendo,11.27,8.89,10.22,1,31.37
6,Tetris,GB,1989,Puzzle,Nintendo,23.2,2.26,4.22,0.58,30.26
7,New Super Mario Bros.,DS,2006,Platform,Nintendo,11.38,9.23,6.5,2.9,30.01
8,Wii Play,Wii,2006,Misc,Nintendo,14.03,9.2,2.93,2.85,29.02
9,New Super Mario Bros. Wii,Wii,2009,Platform,Nintendo,14.59,7.06,4.7,2.26,28.62
10,Duck Hunt,NES,1984,Shooter,Nintendo,26.93,0.63,0.28,0.47,28.31
11,Nintendogs,DS,2005,Simulation,Nintendo,9.07,11,1.93,2.75,24.76
12,Mario Kart DS,DS,2005,Racing,Nintendo,9.81,7.57,4.13,1.92,23.42
13,Pokemon Gold/Pokemon Silver,GB,1999,Role-Playing,Nintendo,9,6.18,7.2,0.71,23.1
14,Wii Fit,Wii,2007,Sports,Nintendo,8.94,8.03,3.6,2.15,22.72
15,Wii Fit Plus,Wii,2009,Sports,Nintendo,9.09,8.59,2.53,1.79,22
16,Kinect Adventures!,X360,2010,Misc,Microsoft Game Studios,14.97,4.94,0.24,1.67,21.82
17,Grand Theft Auto V,PS3,2013,Action,Take-Two Interactive,7.01,9.27,0.97,4.14,21.4
18,Grand Theft Auto: San Andreas,PS2,2004,Action,Take-Two Interactive,9.43,0.4,0.41,10.57,20.81
19,Super Mario World,SNES,1990,Platform,Nintendo,12.78,3.75,3.54,0.55,20.61
20,Brain Age: Train Your Brain in Minutes a Day,DS,2005,Misc,Nintendo,4.75,9.26,4.16,2.05,20.22
";

    // Write to temp file
    let temp_dir = tempfile::TempDir::new().unwrap();
    let csv_path = temp_dir.path().join("test.csv");
    let mut file = std::fs::File::create(&csv_path).unwrap();
    file.write_all(csv_content.as_bytes()).unwrap();

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, TERMINAL_HEIGHT).unwrap();
    harness.open_file(&csv_path).unwrap();

    // Helper function to verify line numbers match content
    fn verify_line_numbers_match_content(screen: &str, context: &str) {
        for screen_line in screen.lines() {
            if !screen_line.contains("│") {
                continue;
            }

            if let Some(bar_pos) = screen_line.find('│') {
                let gutter = &screen_line[..bar_pos];
                let content = &screen_line[bar_pos + "│".len()..];
                let gutter_line_num: Option<u32> = gutter.trim().parse().ok();

                if let Some(gutter_num) = gutter_line_num {
                    let content_trimmed = content.trim_start();
                    let content_num: Option<u32> = content_trimmed
                        .split(',')
                        .next()
                        .and_then(|s| s.trim().parse().ok());

                    if let Some(csv_num) = content_num {
                        assert_eq!(
                            gutter_num, csv_num,
                            "{}: Gutter shows line {} but content starts with row {}\nRow: '{}'\nFull screen:\n{}",
                            context, gutter_num, csv_num, screen_line, screen
                        );
                    }
                }
            }
        }
    }

    harness.render().unwrap();
    let screen = harness.screen_to_string();
    verify_line_numbers_match_content(&screen, "Initial");

    // A SINGLE PageDown should not cause line number desync
    harness
        .send_key(KeyCode::PageDown, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();
    verify_line_numbers_match_content(&screen, "After single PageDown");

    // Multiple PageDown presses should also keep line numbers correct
    for i in 2..=5 {
        harness
            .send_key(KeyCode::PageDown, KeyModifiers::NONE)
            .unwrap();
        harness.render().unwrap();
        let screen = harness.screen_to_string();
        verify_line_numbers_match_content(&screen, &format!("After PageDown #{}", i));
    }

    // And PageUp should also work correctly
    for i in 1..=5 {
        harness
            .send_key(KeyCode::PageUp, KeyModifiers::NONE)
            .unwrap();
        harness.render().unwrap();
        let screen = harness.screen_to_string();
        verify_line_numbers_match_content(&screen, &format!("After PageUp #{}", i));
    }
}

/// Test that wrapped lines with Unicode grapheme clusters are handled correctly
/// Grapheme clusters (like emoji with modifiers or combining characters) should not be split
#[test]
fn test_wrapped_line_with_grapheme_clusters_visible() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Use unique words mixed with grapheme clusters
    // Include: emoji with skin tone modifier, combining diacritics, ZWJ sequences
    let words = vec![
        "30000",
        "👨🏽", // Man with medium skin tone (2 code points, 1 grapheme)
        "30001",
        "café", // With composed é
        "30002",
        "e\u{0301}", // e + combining acute accent (2 code points, 1 grapheme: é)
        "30003",
        "🇺🇸", // Flag (2 regional indicators, 1 grapheme)
        "30004",
        "30005",
        "30006",
        "30007",
        "30008",
        "30009",
    ];
    let text = words.join(" ");
    harness.type_text(&text).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // Verify all number words appear (graphemes might render differently across terminals)
    for word in [
        "30000", "30001", "30002", "30003", "30004", "30005", "30006", "30007", "30008", "30009",
    ] {
        assert!(
            screen.contains(word),
            "Word '{}' is missing from screen output.\nText: {}\nScreen:\n{}",
            word,
            text,
            screen
        );
    }

    // Verify the composed café appears
    assert!(
        screen.contains("café") || screen.contains("cafe"),
        "Word 'café' is missing from screen output.\nScreen:\n{}",
        screen
    );
}

/// Test mouse click on wrapped lines containing Thai grapheme clusters
/// Thai graphemes are multi-code-point but single visual units
#[test]
fn test_mouse_click_wrapped_thai_grapheme_clusters() {
    const TERMINAL_WIDTH: u16 = 40; // Narrow to force wrapping
    const TERMINAL_HEIGHT: u16 = 24;

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, TERMINAL_HEIGHT).unwrap();

    let double_click_delay =
        std::time::Duration::from_millis(harness.config().editor.double_click_time_ms * 2);

    // Create a line with Thai text that will wrap
    // Each Thai grapheme cluster "ที่" is 9 bytes, 3 code points, ~1 visual column
    // We'll create enough content to force wrapping
    // "Hello ที่นี่ World ที่นี่ End" with padding
    let thai_text = "Hello ที่นี่ World ที่นี่ and more text to force wrapping here";

    // Verify our assumptions about the Thai text
    assert!(
        thai_text.contains("ที่"),
        "Text should contain Thai graphemes"
    );

    harness.type_text(thai_text).unwrap();
    harness.render().unwrap();

    let (content_start, _) = harness.content_area_rows();
    let row = content_start as u16;

    harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();
    let (gutter_x, _) = harness.screen_cursor_position();

    // Valid byte boundaries for the text
    let valid_boundaries: Vec<usize> = thai_text
        .char_indices()
        .map(|(i, _)| i)
        .chain(std::iter::once(thai_text.len()))
        .collect();

    // Click on various positions and verify we land on valid boundaries
    for col_offset in [0, 5, 10, 15, 20] {
        harness.sleep(double_click_delay);
        harness.mouse_click(gutter_x + col_offset, row).unwrap();
        harness.render().unwrap();

        let pos = harness.cursor_position();
        assert!(
            valid_boundaries.contains(&pos),
            "Click at col {} should land on valid boundary, got {}. Valid: {:?}",
            col_offset,
            pos,
            &valid_boundaries[..valid_boundaries.len().min(20)]
        );
    }

    // Click past the end of the first visual row - should position correctly
    harness.sleep(double_click_delay);
    harness.mouse_click(gutter_x + 50, row).unwrap();
    harness.render().unwrap();
    let pos = harness.cursor_position();
    assert!(
        valid_boundaries.contains(&pos),
        "Click past end should land on valid boundary, got {}",
        pos
    );

    // If content wraps to second row, test clicking there too
    harness.sleep(double_click_delay);
    harness.mouse_click(gutter_x + 5, row + 1).unwrap();
    harness.render().unwrap();
    let pos = harness.cursor_position();
    assert!(
        valid_boundaries.contains(&pos),
        "Click on wrapped row should land on valid boundary, got {}",
        pos
    );
}

/// Test Up/Down arrow movement moves by visual line (wrapped segment) instead of logical line
/// Current behavior: Up/Down move by logical lines, skipping wrapped portions
/// Expected behavior: Up/Down should move by visual lines when line wrapping is enabled
#[test]
fn test_visual_line_movement_up_down() {
    const TERMINAL_WIDTH: u16 = 60;
    const TERMINAL_HEIGHT: u16 = 24;

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, TERMINAL_HEIGHT).unwrap();

    // Create a long line that will wrap to multiple visual lines
    // With 60 width, 8 gutter, 1 scrollbar = 51 chars per visual line
    // We want at least 3 visual lines, so ~150+ chars
    let long_line = "The quick brown fox jumps over the lazy dog and continues running through the forest, exploring ancient trees and mysterious pathways that wind between towering oaks and silent pines.";

    harness.type_text(long_line).unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text("Second logical line.").unwrap();

    harness.render().unwrap();

    let buffer_content = harness.get_buffer_content().unwrap();
    eprintln!("Buffer: {} bytes", buffer_content.len());
    eprintln!("Long line: {} chars", long_line.len());

    // Move to start of file
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    let (start_x, start_y) = harness.screen_cursor_position();
    let start_pos = harness.cursor_position();
    eprintln!(
        "Start: buffer_pos={}, screen=({}, {})",
        start_pos, start_x, start_y
    );

    assert_eq!(start_pos, 0, "Should start at buffer position 0");
    let gutter_width = harness.editor().active_state().margins.left_total_width() as u16;
    assert_eq!(start_x, gutter_width, "Should be at start of text area");

    // Calculate the text width per visual line (terminal - gutter - scrollbar)
    let text_width = (TERMINAL_WIDTH - gutter_width - 1) as usize;

    // Move cursor to the middle of the first visual line
    for _ in 0..20 {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
    }
    harness.render().unwrap();

    let (mid_x, mid_y) = harness.screen_cursor_position();
    let mid_pos = harness.cursor_position();
    eprintln!(
        "After 20 rights: buffer_pos={}, screen=({}, {})",
        mid_pos, mid_x, mid_y
    );

    assert_eq!(mid_pos, 20, "Should be at buffer position 20");
    assert_eq!(mid_y, start_y, "Should still be on first visual line");

    // Now press Down - with visual line movement, should move to second visual line
    // (the wrapped continuation), not to the second logical line
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let (down_x, down_y) = harness.screen_cursor_position();
    let down_pos = harness.cursor_position();
    eprintln!(
        "After Down: buffer_pos={}, screen=({}, {})",
        down_pos, down_x, down_y
    );

    // With visual line movement:
    // - Cursor should move to next visual line (y + 1)
    // - Buffer position should be around text_width + column (within the wrapped portion)
    // - Should NOT jump to the second logical line

    // The key assertion: cursor should NOT have jumped to the second logical line
    let newline_pos = long_line.len();
    assert!(
        down_pos < newline_pos,
        "With visual line movement, Down from first visual line should move to wrapped portion \
         (position {} should be < newline at {}), but got position {}. \
         This indicates movement is by logical line instead of visual line.",
        down_pos,
        newline_pos,
        down_pos
    );

    // The cursor should have moved down one visual row
    assert_eq!(
        down_y,
        mid_y + 1,
        "Down should move to next visual row (from {} to {}), but moved to {}",
        mid_y,
        mid_y + 1,
        down_y
    );

    // Buffer position should be approximately at the same column but in the wrapped segment
    // (around text_width + original_column). Tolerance is wide because word-boundary
    // wrapping can push the wrap point back several characters to the previous space.
    let expected_pos_approx = text_width + 20; // roughly where we expect cursor
    assert!(
        (down_pos as isize - expected_pos_approx as isize).abs() <= 10,
        "Buffer position {} should be around {} (text_width + column)",
        down_pos,
        expected_pos_approx
    );

    // Now press Up - should go back to first visual line
    harness.send_key(KeyCode::Up, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let (up_x, up_y) = harness.screen_cursor_position();
    let up_pos = harness.cursor_position();
    eprintln!(
        "After Up: buffer_pos={}, screen=({}, {})",
        up_pos, up_x, up_y
    );

    // Should be back on first visual line
    assert_eq!(
        up_y, mid_y,
        "Up should return to first visual row ({}), but went to {}",
        mid_y, up_y
    );

    // Buffer position should be back around position 20 (same column on first segment)
    assert!(
        (up_pos as isize - 20).abs() < 5,
        "Buffer position {} should be around 20 after Up",
        up_pos
    );
}

/// Test that End key navigates to end of visual (wrapped) line segment
/// and Home key navigates to start of visual segment
/// This is the fix for issue #979: End key should go to end of visual line, not physical line
#[test]
fn test_end_key_goes_to_visual_line_end() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // With 60-col terminal, gutter width ~8, scrollbar=1, text width=51
    // Create a line that wraps into 3 segments
    let long_text = "A fast, lightweight terminal text editor written in Rust. Handles files of any size with instant startup, low memory usage, and modern IDE features.";
    harness.type_text(long_text).unwrap();

    // Go to physical line start using Ctrl+Home (document start)
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    harness.render().unwrap();

    assert_eq!(
        harness.cursor_position(),
        0,
        "Should be at physical line start"
    );

    let (start_x, start_y) = harness.screen_cursor_position();
    eprintln!("Cursor at position 0: screen=({}, {})", start_x, start_y);

    // Press End - should go to end of first visual segment (not end of physical line)
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let end_pos_1 = harness.cursor_position();
    let (end_x_1, end_y_1) = harness.screen_cursor_position();
    eprintln!(
        "After first End: pos={}, screen=({}, {})",
        end_pos_1, end_x_1, end_y_1
    );

    // Cursor should still be on the same visual row (y coordinate unchanged)
    assert_eq!(
        end_y_1, start_y,
        "End should keep cursor on same visual row"
    );

    // Cursor should be at end of first segment, NOT at end of physical line
    assert!(
        end_pos_1 < long_text.len(),
        "End should go to visual line end ({}), not physical line end ({})",
        end_pos_1,
        long_text.len()
    );
    assert!(
        end_pos_1 > 0,
        "End should move cursor forward from position 0"
    );

    // Press End again - should go to end of second visual segment
    harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let end_pos_2 = harness.cursor_position();
    let (_end_x_2, end_y_2) = harness.screen_cursor_position();
    eprintln!("After second End: pos={}, screen_y={}", end_pos_2, end_y_2);

    assert!(
        end_pos_2 > end_pos_1,
        "Second End should move past first visual line end"
    );
    // Should be on the next visual row
    assert_eq!(
        end_y_2,
        start_y + 1,
        "Second End should move to next visual row"
    );

    // Keep pressing End until the cursor reaches the physical line end.
    // The exact number of presses depends on the wrap width; we just
    // require that each press advances and that End eventually lands
    // on the physical line end.
    let mut prev_pos = end_pos_2;
    for step in 0..20 {
        harness.send_key(KeyCode::End, KeyModifiers::NONE).unwrap();
        harness.render().unwrap();
        let pos = harness.cursor_position();
        eprintln!("After End #{step}: pos={pos}");
        assert!(
            pos >= prev_pos,
            "End should not move cursor backward; prev={prev_pos}, now={pos}"
        );
        if pos == long_text.len() {
            return;
        }
        prev_pos = pos;
    }
    panic!(
        "End key never reached physical line end ({}) after many presses; \
         final pos={prev_pos}",
        long_text.len()
    );
}

/// Test that Home key navigates to start of visual (wrapped) line segment
/// Companion to test_end_key_goes_to_visual_line_end for issue #979
#[test]
fn test_home_key_goes_to_visual_line_start() {
    let mut harness = EditorTestHarness::new(60, 24).unwrap();

    // Create a line that wraps into 3 segments
    let long_text = "A fast, lightweight terminal text editor written in Rust. Handles files of any size with instant startup, low memory usage, and modern IDE features.";
    harness.type_text(long_text).unwrap();
    harness.render().unwrap();

    // Cursor is at end of text (on last wrapped segment)
    assert_eq!(harness.cursor_position(), long_text.len());

    let (_end_x, end_y) = harness.screen_cursor_position();

    // Press Home - should go to start of the LAST visual segment (not physical line start)
    harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let home_pos_1 = harness.cursor_position();
    let (_home_x_1, home_y_1) = harness.screen_cursor_position();
    eprintln!(
        "After first Home: pos={}, screen_y={}",
        home_pos_1, home_y_1
    );

    // Should still be on the same visual row as before
    assert_eq!(
        home_y_1, end_y,
        "Home should keep cursor on same visual row"
    );
    assert!(
        home_pos_1 > 0,
        "Home from last segment should NOT go to position 0, got {}",
        home_pos_1
    );

    // Press Home again - should go to start of previous visual segment
    harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
    harness.render().unwrap();

    let home_pos_2 = harness.cursor_position();
    eprintln!("After second Home: pos={}", home_pos_2);

    assert!(home_pos_2 < home_pos_1, "Second Home should move backward");

    // Keep pressing Home until the cursor reaches the physical line start.
    // The exact number of presses depends on wrap width.
    let mut prev_pos = home_pos_2;
    for step in 0..20 {
        harness.send_key(KeyCode::Home, KeyModifiers::NONE).unwrap();
        harness.render().unwrap();
        let pos = harness.cursor_position();
        eprintln!("After Home #{step}: pos={pos}");
        assert!(
            pos <= prev_pos,
            "Home should not move cursor forward; prev={prev_pos}, now={pos}"
        );
        if pos == 0 {
            return;
        }
        prev_pos = pos;
    }
    panic!("Home key never reached physical line start after many presses; final pos={prev_pos}");
}

/// Test Alt+Shift+Up/Down (block select) works with line wrapping enabled
/// Note: Block selection moves by logical lines (not visual lines) to maintain
/// consistent block anchor semantics
#[test]
fn test_block_select_with_line_wrap_enabled() {
    const TERMINAL_WIDTH: u16 = 60;
    const TERMINAL_HEIGHT: u16 = 24;

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, TERMINAL_HEIGHT).unwrap();

    // Create multiple lines (short enough to not wrap)
    harness.type_text("Line 1 content").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text("Line 2 content").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text("Line 3 content").unwrap();

    harness.render().unwrap();

    // Move to start of file and position cursor
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    for _ in 0..5 {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
    }
    harness.render().unwrap();

    let start_pos = harness.cursor_position();
    eprintln!("Before block select down: pos={}", start_pos);

    // Press Alt+Shift+Down for block select
    harness
        .send_key(KeyCode::Down, KeyModifiers::ALT | KeyModifiers::SHIFT)
        .unwrap();
    harness.render().unwrap();

    let down_pos = harness.cursor_position();
    eprintln!("After block select down: pos={}", down_pos);

    // Block selection should have started and cursor should have moved to next logical line
    // Line 1 is "Line 1 content\n" (15 chars including newline)
    // Position 5 is after "Line " on line 1
    // After Down, should be at position 5 on line 2 = 15 + 5 = 20
    assert!(
        down_pos > start_pos,
        "Block select down should move cursor to next line"
    );
}

/// Test Ctrl+Alt+Down (add cursor below) works with line wrapping enabled
/// Note: Add cursor operations use logical lines, not visual lines
#[test]
fn test_add_cursor_below_with_line_wrap_enabled() {
    const TERMINAL_WIDTH: u16 = 60;
    const TERMINAL_HEIGHT: u16 = 24;

    let mut harness = EditorTestHarness::new(TERMINAL_WIDTH, TERMINAL_HEIGHT).unwrap();

    // Create multiple lines
    harness.type_text("Line 1").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text("Line 2").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.type_text("Line 3").unwrap();

    harness.render().unwrap();

    // Move to start of file and position cursor
    harness
        .send_key(KeyCode::Home, KeyModifiers::CONTROL)
        .unwrap();
    for _ in 0..3 {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
    }
    harness.render().unwrap();

    let start_pos = harness.cursor_position();
    eprintln!("Before add cursor below: pos={}", start_pos);

    // Press Ctrl+Alt+Down to add cursor below
    harness
        .send_key(KeyCode::Down, KeyModifiers::CONTROL | KeyModifiers::ALT)
        .unwrap();
    harness.render().unwrap();

    // Verify no crash and cursor count increased
    let cursor_count = harness.editor().active_cursors().count();
    assert_eq!(
        cursor_count, 2,
        "Should have 2 cursors after add cursor below"
    );
}

/// Test per-language line wrap: markdown wraps while global wrapping is disabled
#[test]
fn test_per_language_line_wrap_override() {
    let mut config = Config {
        editor: fresh::config::EditorConfig {
            line_wrap: false,
            ..Default::default()
        },
        ..Default::default()
    };

    // Enable line_wrap only for markdown
    config.languages.insert(
        "markdown".to_string(),
        LanguageConfig {
            extensions: vec!["md".to_string(), "markdown".to_string()],
            line_wrap: Some(true),
            ..Default::default()
        },
    );

    let mut harness = EditorTestHarness::with_config(60, 24, config).unwrap();

    // Create a markdown file with a long line that exceeds 60 cols
    let long_md = "This is a very long markdown line that should wrap because per-language line_wrap is enabled for markdown even though global wrapping is off.";
    let fixture = crate::common::fixtures::TestFixture::new("test_wrap.md", long_md).unwrap();
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // The markdown file should be wrapped: we should see text from the line
    // appearing on multiple visual lines (i.e., both the beginning and a
    // portion from mid-line should be visible).
    assert!(
        screen.contains("long markdown line") && screen.contains("per-language"),
        "Markdown file should have line wrapping enabled via per-language config. Screen:\n{}",
        screen
    );

    // Buffer content should still be a single line (wrapping is visual only)
    let content = harness.get_buffer_content().unwrap();
    assert!(
        !content.contains('\n'),
        "Buffer should be a single line (visual wrap only)"
    );
}

/// Test per-language line wrap: non-markdown file stays unwrapped when global is off
#[test]
fn test_per_language_line_wrap_non_matching_language() {
    let mut config = Config {
        editor: fresh::config::EditorConfig {
            line_wrap: false,
            ..Default::default()
        },
        ..Default::default()
    };

    // Enable line_wrap only for markdown
    config.languages.insert(
        "markdown".to_string(),
        LanguageConfig {
            extensions: vec!["md".to_string()],
            line_wrap: Some(true),
            ..Default::default()
        },
    );

    let mut harness = EditorTestHarness::with_config(60, 24, config).unwrap();

    // Create a .txt file with a long line
    let long_txt = "This is a very long plaintext line that should NOT wrap because per-language line_wrap is only enabled for markdown and global wrapping is off.";
    let fixture = crate::common::fixtures::TestFixture::new("test_nowrap.txt", long_txt).unwrap();
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // With wrapping off, the text should be truncated at viewport width.
    // The end of the line should NOT be visible since there's no wrapping.
    assert!(
        !screen.contains("global wrapping is off"),
        "Non-markdown file should NOT wrap when global line_wrap is off. Screen:\n{}",
        screen
    );
}

/// Test wrap_column: text wraps at a specific column, not viewport edge
#[test]
fn test_wrap_column_wraps_at_configured_column() {
    // Use a wide terminal (100 cols) but wrap at column 30
    let config = Config {
        editor: fresh::config::EditorConfig {
            line_wrap: true,
            wrap_column: Some(30),
            ..Default::default()
        },
        ..Default::default()
    };
    let mut harness = EditorTestHarness::with_config(100, 24, config).unwrap();

    // Type text that's longer than 30 chars but shorter than 100
    let text = "AAAA BBBB CCCC DDDD EEEE FFFF GGGG HHHH";
    harness.type_text(text).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // With wrap_column=30 on a 100-col terminal, the text should wrap.
    // Both the beginning and end of the text should be visible (wrapped).
    assert!(
        screen.contains("AAAA") && screen.contains("HHHH"),
        "Text should wrap at column 30 so both start and end are visible. Screen:\n{}",
        screen
    );
}

/// Test per-language wrap_column: markdown wraps at 40 while global is None
#[test]
fn test_per_language_wrap_column() {
    let mut config = Config {
        editor: fresh::config::EditorConfig {
            line_wrap: true,
            ..Default::default()
        },
        ..Default::default()
    };

    config.languages.insert(
        "markdown".to_string(),
        LanguageConfig {
            extensions: vec!["md".to_string()],
            wrap_column: Some(30),
            ..Default::default()
        },
    );

    let mut harness = EditorTestHarness::with_config(100, 24, config).unwrap();

    let long_md = "AAAA BBBB CCCC DDDD EEEE FFFF GGGG HHHH IIII JJJJ";
    let fixture = crate::common::fixtures::TestFixture::new("test_wrap_col.md", long_md).unwrap();
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // Markdown should wrap at column 30 despite wide terminal.
    // Both start and end should be visible.
    assert!(
        screen.contains("AAAA") && screen.contains("JJJJ"),
        "Markdown should wrap at column 30. Screen:\n{}",
        screen
    );
}

/// Test per-language page_view auto-activation: markdown opens in page view mode
#[test]
fn test_per_language_page_view_auto_activation() {
    let mut config = Config::default();

    // Enable page_view for markdown with a page_width
    config.languages.insert(
        "markdown".to_string(),
        LanguageConfig {
            extensions: vec!["md".to_string()],
            page_view: Some(true),
            page_width: Some(60),
            ..Default::default()
        },
    );

    let mut harness = EditorTestHarness::with_config(100, 24, config).unwrap();

    let md_content = "# Hello World\n\nSome markdown content here.";
    let fixture =
        crate::common::fixtures::TestFixture::new("test_page_view.md", md_content).unwrap();
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // In page view mode, line numbers should be hidden.
    // A normal file shows "  1 │" gutter; in page view this disappears.
    assert!(
        !screen.contains(" 1 │"),
        "Page view should hide line numbers for markdown. Screen:\n{}",
        screen
    );

    // Content should still be visible
    assert!(
        screen.contains("Hello World") || screen.contains("markdown content"),
        "Markdown content should still be visible in page view. Screen:\n{}",
        screen
    );
}

/// Test page_view does NOT activate for non-matching languages
#[test]
fn test_page_view_not_activated_for_other_languages() {
    let mut config = Config::default();

    // Enable page_view only for markdown
    config.languages.insert(
        "markdown".to_string(),
        LanguageConfig {
            extensions: vec!["md".to_string()],
            page_view: Some(true),
            ..Default::default()
        },
    );

    let mut harness = EditorTestHarness::with_config(100, 24, config).unwrap();

    let txt_content = "Just a plain text file.";
    let fixture =
        crate::common::fixtures::TestFixture::new("test_no_page_view.txt", txt_content).unwrap();
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // Non-markdown file should show line numbers (not in page view)
    assert!(
        screen.contains(" 1 │") || screen.contains("1 │"),
        "Non-markdown file should show line numbers (no page view). Screen:\n{}",
        screen
    );
}

/// Test that page view defaults to 80 columns on a wide terminal
#[test]
fn test_page_view_default_width_80() {
    use crossterm::event::{KeyCode, KeyModifiers};

    // Use a wide terminal so 80-col centering is visible
    let mut harness = EditorTestHarness::new(160, 24).unwrap();

    // Create a file with content that fits within 80 columns
    let content = "Hello from page view test";
    let fixture =
        crate::common::fixtures::TestFixture::new("test_default_width.txt", content).unwrap();
    harness.open_file(&fixture.path).unwrap();
    harness.render().unwrap();

    // Toggle page view on
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.type_text("Toggle Page View").unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.render().unwrap();

    let screen = harness.screen_to_string();

    // Find the row containing our content
    let mut content_row = String::new();
    let height = 24u16;
    for y in 0..height {
        let row = harness.get_row_text(y);
        if row.contains("Hello from") {
            content_row = row;
            break;
        }
    }
    assert!(
        !content_row.is_empty(),
        "Should find a row with content\nScreen:\n{}",
        screen
    );

    let trimmed = content_row.trim();

    // Content should be at most 80 chars wide
    assert!(
        trimmed.len() <= 80,
        "Page view content should be at most 80 columns wide, got {} chars: '{}'\nScreen:\n{}",
        trimmed.len(),
        trimmed,
        screen
    );

    // There should be left margin (centering) since terminal is 160 cols
    let leading_spaces = content_row.len() - content_row.trim_start().len();
    assert!(
        leading_spaces >= 30,
        "Expected centering margin (>=30 cols) on 160-col terminal, got {} leading spaces\nRow: '{}'\nScreen:\n{}",
        leading_spaces,
        content_row,
        screen
    );
}
