use super::super::*;
use crate::test_utils::test_helpers::{key, key_with_mods, test_app};
use proptest::prelude::*;
use ratatui::crossterm::event::{KeyCode, KeyModifiers};
use std::sync::Arc;

#[test]
fn test_navigation_scrolls_to_match() {
    let mut app = test_app(r#"{"name": "test"}"#);

    let content: String = (0..30)
        .map(|i| {
            if i == 0 || i == 10 || i == 20 {
                format!("match line {}\n", i)
            } else {
                format!("other line {}\n", i)
            }
        })
        .collect();

    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
    app.query
        .as_mut()
        .unwrap()
        .last_successful_result_unformatted = Some(Arc::new(content.clone()));

    app.results_scroll.viewport_height = 10;
    app.results_scroll.max_offset = 20;
    app.results_scroll.offset = 0;

    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("match");
    app.search.update_matches(&content);

    assert_eq!(app.search.matches().len(), 3);
    assert_eq!(app.search.matches()[0].line, 0);
    assert_eq!(app.search.matches()[1].line, 10);
    assert_eq!(app.search.matches()[2].line, 20);

    handle_search_key(&mut app, key(KeyCode::Enter));
    assert!(app.search.is_confirmed());
    assert_eq!(app.search.current_index(), 0);

    assert_eq!(
        app.results_scroll.offset, 0,
        "Scroll should be at top for match at line 0"
    );

    handle_search_key(&mut app, key(KeyCode::Char('n')));
    assert_eq!(app.search.current_index(), 1);

    assert_eq!(
        app.results_scroll.offset, 6,
        "Scroll should position match at line 10 with bottom margin"
    );

    handle_search_key(&mut app, key(KeyCode::Char('n')));
    assert_eq!(app.search.current_index(), 2);

    assert_eq!(
        app.results_scroll.offset, 16,
        "Scroll should position match at line 20 with bottom margin"
    );

    handle_search_key(&mut app, key(KeyCode::Char('n')));
    assert_eq!(app.search.current_index(), 0);

    assert_eq!(
        app.results_scroll.offset, 0,
        "Scroll should be at top for match at line 0"
    );
}

#[test]
fn test_n_navigates_to_next_match() {
    let mut app = test_app(r#"{"name": "test"}"#);
    app.query.as_mut().unwrap().last_successful_result =
        Some(Arc::new("test\ntest\ntest".to_string()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches("test\ntest\ntest");

    assert_eq!(app.search.matches().len(), 3);
    assert_eq!(app.search.current_index(), 0);

    handle_search_key(&mut app, key(KeyCode::Enter));
    assert!(app.search.is_confirmed());
    assert_eq!(app.search.current_index(), 0);

    handle_search_key(&mut app, key(KeyCode::Char('n')));
    assert_eq!(app.search.current_index(), 1);

    handle_search_key(&mut app, key(KeyCode::Char('n')));
    assert_eq!(app.search.current_index(), 2);
}

#[test]
fn test_capital_n_navigates_to_prev_match() {
    let mut app = test_app(r#"{"name": "test"}"#);
    app.query.as_mut().unwrap().last_successful_result =
        Some(Arc::new("test\ntest\ntest".to_string()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches("test\ntest\ntest");

    handle_search_key(&mut app, key(KeyCode::Enter));
    assert!(app.search.is_confirmed());
    assert_eq!(app.search.current_index(), 0);

    handle_search_key(&mut app, key(KeyCode::Char('N')));
    assert_eq!(app.search.current_index(), 2);
}

#[test]
fn test_enter_navigates_to_next_match() {
    let mut app = test_app(r#"{"name": "test"}"#);
    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new("test\ntest".to_string()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches("test\ntest");

    handle_search_key(&mut app, key(KeyCode::Enter));
    assert_eq!(app.search.current_index(), 0);
    assert!(app.search.is_confirmed());

    handle_search_key(&mut app, key(KeyCode::Enter));
    assert_eq!(app.search.current_index(), 1);
}

#[test]
fn test_shift_enter_navigates_to_prev_match() {
    let mut app = test_app(r#"{"name": "test"}"#);
    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new("test\ntest".to_string()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches("test\ntest");

    handle_search_key(&mut app, key_with_mods(KeyCode::Enter, KeyModifiers::SHIFT));
    assert_eq!(app.search.current_index(), 0);
    assert!(app.search.is_confirmed());

    handle_search_key(&mut app, key_with_mods(KeyCode::Enter, KeyModifiers::SHIFT));
    assert_eq!(app.search.current_index(), 1);
}

#[test]
fn test_ctrl_f_reenters_edit_mode_when_confirmed() {
    let mut app = test_app(r#"{"name": "test"}"#);
    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new("test\ntest".to_string()));
    app.query
        .as_mut()
        .unwrap()
        .last_successful_result_unformatted = Some(Arc::new("test\ntest".to_string()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches("test\ntest");

    handle_search_key(&mut app, key(KeyCode::Enter));
    assert!(app.search.is_confirmed());

    handle_search_key(
        &mut app,
        key_with_mods(KeyCode::Char('f'), KeyModifiers::CONTROL),
    );

    assert!(!app.search.is_confirmed());
    assert!(app.search.is_visible());
    assert_eq!(app.search.query(), "test");
}

#[test]
fn test_slash_reenters_edit_mode_when_confirmed() {
    let mut app = test_app(r#"{"name": "test"}"#);
    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new("test\ntest".to_string()));
    app.query
        .as_mut()
        .unwrap()
        .last_successful_result_unformatted = Some(Arc::new("test\ntest".to_string()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches("test\ntest");

    handle_search_key(&mut app, key(KeyCode::Enter));
    assert!(app.search.is_confirmed());

    handle_search_key(&mut app, key(KeyCode::Char('/')));

    assert!(!app.search.is_confirmed());
    assert!(app.search.is_visible());
    assert_eq!(app.search.query(), "test");
}

#[test]
fn test_can_type_after_reenter_edit_mode() {
    let mut app = test_app(r#"{"name": "test"}"#);
    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new("test\ntest".to_string()));
    app.query
        .as_mut()
        .unwrap()
        .last_successful_result_unformatted = Some(Arc::new("test\ntest".to_string()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches("test\ntest");

    handle_search_key(&mut app, key(KeyCode::Enter));
    assert!(app.search.is_confirmed());

    handle_search_key(
        &mut app,
        key_with_mods(KeyCode::Char('f'), KeyModifiers::CONTROL),
    );
    assert!(!app.search.is_confirmed());

    handle_search_key(&mut app, key(KeyCode::Char('2')));

    assert_eq!(app.search.query(), "test2");
}

#[test]
fn test_tab_confirms_search_when_not_confirmed() {
    let mut app = test_app(r#"{"name": "test"}"#);
    let content = "test\ntest\ntest".to_string();
    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
    app.query
        .as_mut()
        .unwrap()
        .last_successful_result_unformatted = Some(Arc::new(content.clone()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches(&content);

    assert!(!app.search.is_confirmed());
    assert_eq!(app.search.current_index(), 0);

    handle_search_key(&mut app, key(KeyCode::Tab));

    assert!(app.search.is_confirmed());
    assert_eq!(app.search.current_index(), 0);
}

#[test]
fn test_tab_unconfirms_search_when_confirmed() {
    use crate::app::Focus;

    let mut app = test_app(r#"{"name": "test"}"#);
    let content = "test\ntest\ntest".to_string();
    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
    app.query
        .as_mut()
        .unwrap()
        .last_successful_result_unformatted = Some(Arc::new(content.clone()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches(&content);
    handle_search_key(&mut app, key(KeyCode::Enter));

    assert!(app.search.is_confirmed());
    assert_eq!(app.focus, Focus::ResultsPane);

    handle_search_key(&mut app, key(KeyCode::Tab));

    assert!(!app.search.is_confirmed());
    assert!(app.search.is_visible());
    assert_eq!(app.search.query(), "test");
    assert_eq!(app.focus, Focus::ResultsPane);
}

#[test]
fn test_tab_cycles_between_edit_and_navigation_mode() {
    let mut app = test_app(r#"{"name": "test"}"#);
    let content = "test\ntest\ntest".to_string();
    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
    app.query
        .as_mut()
        .unwrap()
        .last_successful_result_unformatted = Some(Arc::new(content.clone()));
    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches(&content);

    assert!(!app.search.is_confirmed());

    handle_search_key(&mut app, key(KeyCode::Tab));
    assert!(app.search.is_confirmed());

    handle_search_key(&mut app, key(KeyCode::Tab));
    assert!(!app.search.is_confirmed());

    handle_search_key(&mut app, key(KeyCode::Tab));
    assert!(app.search.is_confirmed());

    assert_eq!(app.search.query(), "test");
}

#[test]
fn test_tab_scrolls_to_current_match_when_confirming() {
    let mut app = test_app(r#"{"name": "test"}"#);

    let content: String = (0..30)
        .map(|i| {
            if i == 0 || i == 15 {
                format!("match line {}\n", i)
            } else {
                format!("other line {}\n", i)
            }
        })
        .collect();

    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
    app.query
        .as_mut()
        .unwrap()
        .last_successful_result_unformatted = Some(Arc::new(content.clone()));

    app.results_scroll.viewport_height = 10;
    app.results_scroll.max_offset = 20;
    app.results_scroll.offset = 0;

    open_search(&mut app);

    app.search.search_textarea_mut().insert_str("match");
    app.search.update_matches(&content);

    assert_eq!(app.search.matches().len(), 2);
    assert_eq!(app.search.matches()[0].line, 0);

    handle_search_key(&mut app, key(KeyCode::Tab));

    assert!(app.search.is_confirmed());
    assert_eq!(
        app.results_scroll.offset, 0,
        "Should scroll to first match at line 0"
    );
}

fn app_with_confirmed_search() -> crate::app::App {
    let mut app = test_app(r#"{"name": "test"}"#);

    let content: String = (0..50).map(|i| format!("line {} test\n", i)).collect();
    app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
    app.query
        .as_mut()
        .unwrap()
        .last_successful_result_unformatted = Some(Arc::new(content.clone()));
    app.query.as_mut().unwrap().result = Ok(content.clone());

    app.results_scroll.update_bounds(50, 10);
    app.results_scroll.update_h_bounds(100, 40);
    app.results_scroll.offset = 0;
    app.results_scroll.h_offset = 0;

    app.results_cursor.update_total_lines(50);

    open_search(&mut app);
    app.search.search_textarea_mut().insert_str("test");
    app.search.update_matches(&content);
    handle_search_key(&mut app, key(KeyCode::Enter));

    assert!(app.search.is_confirmed());
    app
}

#[test]
fn test_j_moves_cursor_down_when_search_confirmed() {
    let mut app = app_with_confirmed_search();

    handle_search_key(&mut app, key(KeyCode::Char('j')));

    assert_eq!(app.results_cursor.cursor_line(), 1);
    assert!(app.search.is_confirmed(), "Search should remain confirmed");
}

#[test]
fn test_k_moves_cursor_up_when_search_confirmed() {
    let mut app = app_with_confirmed_search();
    app.results_cursor.move_to_line(10);

    handle_search_key(&mut app, key(KeyCode::Char('k')));

    assert_eq!(app.results_cursor.cursor_line(), 9);
    assert!(app.search.is_confirmed(), "Search should remain confirmed");
}

#[test]
fn test_h_scrolls_left_when_search_confirmed() {
    let mut app = app_with_confirmed_search();
    app.results_scroll.h_offset = 10;

    handle_search_key(&mut app, key(KeyCode::Char('h')));

    assert_eq!(app.results_scroll.h_offset, 9);
    assert!(app.search.is_confirmed(), "Search should remain confirmed");
}

#[test]
fn test_l_scrolls_right_when_search_confirmed() {
    let mut app = app_with_confirmed_search();
    app.results_scroll.h_offset = 0;

    handle_search_key(&mut app, key(KeyCode::Char('l')));

    assert_eq!(app.results_scroll.h_offset, 1);
    assert!(app.search.is_confirmed(), "Search should remain confirmed");
}

#[test]
fn test_g_jumps_to_top_when_search_confirmed() {
    let mut app = app_with_confirmed_search();
    app.results_cursor.move_to_line(30);
    app.results_scroll.offset = 20;

    handle_search_key(&mut app, key(KeyCode::Char('g')));

    assert_eq!(app.results_cursor.cursor_line(), 0);
    assert_eq!(app.results_scroll.offset, 0);
    assert!(app.search.is_confirmed(), "Search should remain confirmed");
}

#[test]
fn test_capital_g_jumps_to_bottom_when_search_confirmed() {
    let mut app = app_with_confirmed_search();

    handle_search_key(&mut app, key(KeyCode::Char('G')));

    assert_eq!(app.results_cursor.cursor_line(), 49);
    assert!(app.search.is_confirmed(), "Search should remain confirmed");
}

#[test]
fn test_ctrl_d_page_down_when_search_confirmed() {
    let mut app = app_with_confirmed_search();

    handle_search_key(
        &mut app,
        key_with_mods(KeyCode::Char('d'), KeyModifiers::CONTROL),
    );

    assert_eq!(app.results_cursor.cursor_line(), 5);
    assert!(app.search.is_confirmed(), "Search should remain confirmed");
}

#[test]
fn test_ctrl_u_page_up_when_search_confirmed() {
    let mut app = app_with_confirmed_search();
    app.results_cursor.move_to_line(20);

    handle_search_key(
        &mut app,
        key_with_mods(KeyCode::Char('u'), KeyModifiers::CONTROL),
    );

    assert_eq!(app.results_cursor.cursor_line(), 15);
    assert!(app.search.is_confirmed(), "Search should remain confirmed");
}

#[test]
fn test_navigation_preserves_match_index() {
    let mut app = app_with_confirmed_search();

    handle_search_key(&mut app, key(KeyCode::Char('n')));
    let match_index_before = app.search.current_index();

    handle_search_key(&mut app, key(KeyCode::Char('j')));
    handle_search_key(&mut app, key(KeyCode::Char('k')));
    handle_search_key(&mut app, key(KeyCode::Char('l')));
    handle_search_key(&mut app, key(KeyCode::Char('h')));

    assert_eq!(app.search.current_index(), match_index_before);
}

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

    #[test]
    fn prop_ctrl_f_opens_search_from_any_pane(
        focus_on_input in any::<bool>(),
    ) {
        use crate::app::Focus;
        let mut app = test_app(r#"{"name": "test"}"#);

        app.focus = if focus_on_input {
            Focus::InputField
        } else {
            Focus::ResultsPane
        };

        assert!(!app.search.is_visible());

        open_search(&mut app);

        prop_assert!(
            app.search.is_visible(),
            "Search should be visible after Ctrl+F"
        );

        prop_assert_eq!(
            app.focus, Focus::ResultsPane,
            "Focus should be on results pane after Ctrl+F"
        );
    }
}

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

    #[test]
    fn prop_slash_opens_search_only_from_results_pane(
        focus_on_input in any::<bool>(),
    ) {
        use crate::app::Focus;
        let mut app = test_app(r#"{"name": "test"}"#);

        let initial_focus = if focus_on_input {
            Focus::InputField
        } else {
            Focus::ResultsPane
        };
        app.focus = initial_focus;

        assert!(!app.search.is_visible());

        if initial_focus == Focus::ResultsPane {
            open_search(&mut app);

            prop_assert!(
                app.search.is_visible(),
                "Search should be visible after '/' in results pane"
            );
        } else {
            prop_assert!(
                !app.search.is_visible(),
                "Search should NOT be visible when '/' pressed in input field"
            );
        }
    }
}

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

    #[test]
    fn prop_vertical_navigation_scrolls_when_search_confirmed(
        viewport_height in 5u16..50,
        max_offset in 10u16..200,
        initial_cursor_factor in 0.0f64..1.0,
        key_type in 0u8..10,
    ) {
        let mut app = test_app(r#"{"name": "test"}"#);

        let content_lines = max_offset as u32 + viewport_height as u32;
        let content: String = (0..content_lines)
            .map(|i| format!("line {} test\n", i))
            .collect();
        app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
        app.query.as_mut().unwrap().last_successful_result_unformatted = Some(Arc::new(content.clone()));
        app.query.as_mut().unwrap().result = Ok(content.clone());

        app.results_scroll.update_bounds(content_lines, viewport_height);
        app.results_cursor.update_total_lines(content_lines);

        let max_cursor = content_lines.saturating_sub(1);
        let initial_cursor = ((initial_cursor_factor * max_cursor as f64) as u32).min(max_cursor);
        app.results_cursor.move_to_line(initial_cursor);

        open_search(&mut app);
        app.search.search_textarea_mut().insert_str("test");
        app.search.update_matches(&content);
        handle_search_key(&mut app, key(KeyCode::Enter));

        prop_assert!(app.search.is_confirmed(), "Search should be confirmed");

        let cursor_before = app.results_cursor.cursor_line();

        let test_key = match key_type {
            0 => key(KeyCode::Char('j')),
            1 => key(KeyCode::Char('k')),
            2 => key(KeyCode::Char('J')),
            3 => key(KeyCode::Char('K')),
            4 => key(KeyCode::Char('g')),
            5 => key(KeyCode::Char('G')),
            6 => key(KeyCode::Up),
            7 => key(KeyCode::Down),
            8 => key(KeyCode::Home),
            9 => key(KeyCode::End),
            _ => key(KeyCode::Char('j')),
        };

        handle_search_key(&mut app, test_key);

        let expected_cursor = match key_type {
            0 | 7 => cursor_before.saturating_add(1).min(max_cursor),
            1 | 6 => cursor_before.saturating_sub(1),
            2 => cursor_before.saturating_add(10).min(max_cursor),
            3 => cursor_before.saturating_sub(10),
            4 | 8 => 0,
            5 | 9 => max_cursor,
            _ => cursor_before,
        };

        prop_assert_eq!(
            app.results_cursor.cursor_line(), expected_cursor,
            "Vertical navigation key {} should change cursor from {} to {}, got {}",
            key_type, cursor_before, expected_cursor, app.results_cursor.cursor_line()
        );

        prop_assert!(app.search.is_confirmed(), "Search should remain confirmed after navigation");
    }
}

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

    #[test]
    fn prop_horizontal_navigation_scrolls_when_search_confirmed(
        viewport_width in 20u16..80,
        max_h_offset in 20u16..200,
        initial_h_offset_factor in 0.0f64..1.0,
        key_type in 0u8..7,
    ) {
        let mut app = test_app(r#"{"name": "test"}"#);

        let line_width = max_h_offset + viewport_width;
        let content: String = (0..20)
            .map(|i| format!("line {} test {}\n", i, "x".repeat(line_width as usize)))
            .collect();
        app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
        app.query.as_mut().unwrap().last_successful_result_unformatted = Some(Arc::new(content.clone()));
        app.query.as_mut().unwrap().result = Ok(content.clone());

        app.results_scroll.update_bounds(20, 10);
        app.results_scroll.update_h_bounds(line_width, viewport_width);
        let initial_h_offset = ((initial_h_offset_factor * max_h_offset as f64) as u16).min(max_h_offset);
        app.results_scroll.h_offset = initial_h_offset;

        open_search(&mut app);
        app.search.search_textarea_mut().insert_str("test");
        app.search.update_matches(&content);
        handle_search_key(&mut app, key(KeyCode::Enter));

        prop_assert!(app.search.is_confirmed(), "Search should be confirmed");

        let h_offset_before = app.results_scroll.h_offset;

        let test_key = match key_type {
            0 => key(KeyCode::Char('h')),
            1 => key(KeyCode::Char('l')),
            2 => key(KeyCode::Char('H')),
            3 => key(KeyCode::Char('L')),
            4 => key(KeyCode::Char('0')),
            5 => key(KeyCode::Char('^')),
            6 => key(KeyCode::Left),
            _ => key(KeyCode::Char('h')),
        };

        handle_search_key(&mut app, test_key);

        let expected_h_offset = match key_type {
            0 | 6 => h_offset_before.saturating_sub(1),
            1 => h_offset_before.saturating_add(1).min(max_h_offset),
            2 => h_offset_before.saturating_sub(10),
            3 => h_offset_before.saturating_add(10).min(max_h_offset),
            4 | 5 => 0,
            _ => h_offset_before,
        };

        prop_assert_eq!(
            app.results_scroll.h_offset, expected_h_offset,
            "Horizontal navigation key {} should change h_offset from {} to {}, got {}",
            key_type, h_offset_before, expected_h_offset, app.results_scroll.h_offset
        );

        prop_assert!(app.search.is_confirmed(), "Search should remain confirmed after navigation");
    }
}

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

    #[test]
    fn prop_page_scroll_when_search_confirmed(
        viewport_height in 10u16..50,
        max_offset in 20u16..200,
        initial_cursor_factor in 0.0f64..1.0,
        key_type in 0u8..4,
    ) {
        let mut app = test_app(r#"{"name": "test"}"#);

        let content_lines = max_offset as u32 + viewport_height as u32;
        let content: String = (0..content_lines)
            .map(|i| format!("line {} test\n", i))
            .collect();
        app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
        app.query.as_mut().unwrap().last_successful_result_unformatted = Some(Arc::new(content.clone()));
        app.query.as_mut().unwrap().result = Ok(content.clone());

        app.results_scroll.update_bounds(content_lines, viewport_height);
        app.results_cursor.update_total_lines(content_lines);

        let max_cursor = content_lines.saturating_sub(1);
        let initial_cursor = ((initial_cursor_factor * max_cursor as f64) as u32).min(max_cursor);
        app.results_cursor.move_to_line(initial_cursor);

        open_search(&mut app);
        app.search.search_textarea_mut().insert_str("test");
        app.search.update_matches(&content);
        handle_search_key(&mut app, key(KeyCode::Enter));

        prop_assert!(app.search.is_confirmed(), "Search should be confirmed");

        let cursor_before = app.results_cursor.cursor_line();
        let half_page = viewport_height / 2;

        let test_key = match key_type {
            0 => key_with_mods(KeyCode::Char('d'), KeyModifiers::CONTROL),
            1 => key_with_mods(KeyCode::Char('u'), KeyModifiers::CONTROL),
            2 => key(KeyCode::PageDown),
            3 => key(KeyCode::PageUp),
            _ => key(KeyCode::PageDown),
        };

        handle_search_key(&mut app, test_key);

        let expected_cursor = match key_type {
            0 | 2 => cursor_before.saturating_add(half_page as u32).min(max_cursor),
            1 | 3 => cursor_before.saturating_sub(half_page as u32),
            _ => cursor_before,
        };

        prop_assert_eq!(
            app.results_cursor.cursor_line(), expected_cursor,
            "Page scroll key {} should change cursor from {} to {} (half_page={}), got {}",
            key_type, cursor_before, expected_cursor, half_page, app.results_cursor.cursor_line()
        );

        prop_assert!(app.search.is_confirmed(), "Search should remain confirmed after navigation");
    }
}

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

    #[test]
    fn prop_navigation_preserves_match_index(
        viewport_height in 10u16..50,
        max_offset in 20u16..200,
        initial_cursor_factor in 0.0f64..1.0,
        n_presses in 0usize..6,
        key_type in 0u8..15,
    ) {
        let mut app = test_app(r#"{"name": "test"}"#);

        let content_lines = max_offset as u32 + viewport_height as u32;
        let content: String = (0..content_lines)
            .map(|i| format!("line {} test\n", i))
            .collect();
        app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
        app.query.as_mut().unwrap().last_successful_result_unformatted = Some(Arc::new(content.clone()));
        app.query.as_mut().unwrap().result = Ok(content.clone());

        app.results_scroll.update_bounds(content_lines, viewport_height);
        app.results_scroll.update_h_bounds(100, 40);
        app.results_cursor.update_total_lines(content_lines);

        let max_cursor = content_lines.saturating_sub(1);
        let initial_cursor = ((initial_cursor_factor * max_cursor as f64) as u32).min(max_cursor);
        app.results_cursor.move_to_line(initial_cursor);
        app.results_scroll.h_offset = 10;

        open_search(&mut app);
        app.search.search_textarea_mut().insert_str("test");
        app.search.update_matches(&content);
        handle_search_key(&mut app, key(KeyCode::Enter));

        prop_assert!(app.search.is_confirmed(), "Search should be confirmed");
        prop_assert!(!app.search.matches().is_empty(), "Should have matches");

        for _ in 0..n_presses {
            handle_search_key(&mut app, key(KeyCode::Char('n')));
        }

        let match_index_before = app.search.current_index();

        let test_key = match key_type {
            0 => key(KeyCode::Char('j')),
            1 => key(KeyCode::Char('k')),
            2 => key(KeyCode::Char('J')),
            3 => key(KeyCode::Char('K')),
            4 => key(KeyCode::Char('g')),
            5 => key(KeyCode::Char('G')),
            6 => key(KeyCode::Char('h')),
            7 => key(KeyCode::Char('l')),
            8 => key(KeyCode::Char('H')),
            9 => key(KeyCode::Char('L')),
            10 => key(KeyCode::Char('0')),
            11 => key(KeyCode::Char('^')),
            12 => key_with_mods(KeyCode::Char('d'), KeyModifiers::CONTROL),
            13 => key_with_mods(KeyCode::Char('u'), KeyModifiers::CONTROL),
            14 => key(KeyCode::PageDown),
            _ => key(KeyCode::Char('j')),
        };

        handle_search_key(&mut app, test_key);

        prop_assert_eq!(
            app.search.current_index(), match_index_before,
            "Navigation key {} should not change match index (was {}, now {})",
            key_type, match_index_before, app.search.current_index()
        );

        prop_assert!(app.search.is_confirmed(), "Search should remain confirmed after navigation");
    }
}

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

    #[test]
    fn prop_scroll_preserves_search_state(
        num_matches in 1usize..20,
        query in "[a-zA-Z]{1,10}",
        viewport_height in 10u16..50,
        max_offset in 20u16..200,
        initial_offset in 0u16..100,
        scroll_op in 0u8..8,
    ) {
        use crate::search::search_state::Match;

        let mut app = test_app(r#"{"name": "test"}"#);

        app.search.open();
        app.search.search_textarea_mut().insert_str(&query);

        let content: String = (0..num_matches)
            .map(|i| format!("line {} {}\n", i, query))
            .collect();
        app.query.as_mut().unwrap().last_successful_result = Some(Arc::new(content.clone()));
        app.search.update_matches(&content);

        let matches_before: Vec<Match> = app.search.matches().to_vec();
        let current_index_before = app.search.current_index();
        let query_before = app.search.query().to_string();
        let visible_before = app.search.is_visible();

        app.results_scroll.viewport_height = viewport_height;
        app.results_scroll.max_offset = max_offset;
        app.results_scroll.offset = initial_offset.min(max_offset);

        match scroll_op {
            0 => app.results_scroll.scroll_up(1),
            1 => app.results_scroll.scroll_down(1),
            2 => app.results_scroll.scroll_up(10),
            3 => app.results_scroll.scroll_down(10),
            4 => app.results_scroll.page_up(),
            5 => app.results_scroll.page_down(),
            6 => app.results_scroll.jump_to_top(),
            7 => app.results_scroll.jump_to_bottom(),
            _ => app.results_scroll.scroll_down(1),
        }

        prop_assert_eq!(
            app.search.matches().to_vec(), matches_before,
            "Matches should be unchanged after scroll"
        );
        prop_assert_eq!(
            app.search.current_index(), current_index_before,
            "Current index should be unchanged after scroll"
        );
        prop_assert_eq!(
            app.search.query(), query_before,
            "Query should be unchanged after scroll"
        );
        prop_assert_eq!(
            app.search.is_visible(), visible_before,
            "Visibility should be unchanged after scroll"
        );
    }
}
