use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
use ratatui::{backend::TestBackend, Terminal};
use rgx::app::App;
use rgx::engine::{EngineFlags, EngineKind};
use rgx::input::handler::handle_help_key;
use rgx::ui::{self, build_lengths_of_help_pages};

fn create_test_terminal() -> Terminal<TestBackend> {
    let backend = TestBackend::new(80, 24);
    Terminal::new(backend).unwrap()
}

const fn press_key(code: KeyCode) -> KeyEvent {
    KeyEvent {
        code,
        modifiers: KeyModifiers::empty(),
        kind: KeyEventKind::Press,
        state: KeyEventState::empty(),
    }
}

#[test]
fn render_empty_state() {
    let mut terminal = create_test_terminal();
    let app = App::new(EngineKind::RustRegex, EngineFlags::default());
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
}

#[test]
fn render_with_pattern() {
    let mut terminal = create_test_terminal();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("hello 123 world 456");
    app.set_pattern(r"\d+");
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
    assert_eq!(app.matches.len(), 2);
}

#[test]
fn render_with_error() {
    let mut terminal = create_test_terminal();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_pattern(r"(unclosed");
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
    assert!(app.error.is_some());
}

#[test]
fn render_with_captures() {
    let mut terminal = create_test_terminal();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("user@example.com");
    app.set_pattern(r"(\w+)@(\w+)\.(\w+)");
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
    assert_eq!(app.matches.len(), 1);
    assert_eq!(app.matches[0].captures.len(), 3);
}

#[test]
fn render_help_overlay() {
    let mut terminal = create_test_terminal();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.overlay.help = true;
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
}

#[test]
fn engine_switching() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("hello 123");
    app.set_pattern(r"\d+");
    assert_eq!(app.matches.len(), 1);

    app.switch_engine();
    assert_eq!(app.engine_kind, EngineKind::FancyRegex);
    assert_eq!(app.matches.len(), 1);
}

#[test]
fn flag_toggles() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("Hello HELLO hello");
    app.set_pattern("hello");
    assert_eq!(app.matches.len(), 1); // only lowercase match

    app.flags.toggle_case_insensitive();
    app.recompute();
    assert_eq!(app.matches.len(), 3); // all match now
}

#[test]
fn match_display_shows_results() {
    let mut terminal = create_test_terminal();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("user@example");
    app.set_pattern(r"(\w+)@(\w+)");

    // Verify match data exists
    assert_eq!(app.matches.len(), 1);
    assert_eq!(app.matches[0].text, "user@example");
    assert_eq!(app.matches[0].captures.len(), 2);

    // Render and check that match text appears in the buffer
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
    let buffer = terminal.backend().buffer().clone();
    let buffer_text: String = buffer
        .content()
        .iter()
        .map(|cell| cell.symbol().chars().next().unwrap_or(' '))
        .collect();
    assert!(
        buffer_text.contains("Match 1"),
        "Buffer should contain 'Match 1' but got: {buffer_text}"
    );
}

#[test]
fn multiline_test_string_renders() {
    let mut terminal = create_test_terminal();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("hello\nworld");
    app.set_pattern(r"\w+");

    // Should find matches on both lines
    assert_eq!(app.matches.len(), 2);
    assert_eq!(app.matches[0].text, "hello");
    assert_eq!(app.matches[1].text, "world");

    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
}

#[test]
fn narrow_terminal_layout() {
    // Test that narrow terminals don't crash
    let backend = TestBackend::new(40, 24);
    let mut terminal = Terminal::new(backend).unwrap();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("test");
    app.set_pattern(r"\w+");
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
}

#[test]
fn render_with_replacement() {
    let mut terminal = create_test_terminal();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("hello 123 world 456");
    app.set_pattern(r"\d+");
    app.set_replacement("[NUM]");

    assert!(app.replace_result.is_some());
    let result = app.replace_result.as_ref().unwrap();
    assert_eq!(result.output, "hello [NUM] world [NUM]");

    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
}

#[test]
fn render_empty_replacement() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("hello 123");
    app.set_pattern(r"\d+");
    // No replacement set
    assert!(app.replace_result.is_none());
}

#[test]
fn panel_cycling_includes_replace() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    assert_eq!(app.focused_panel, 0);
    app.focused_panel = (app.focused_panel + 1) % 5;
    assert_eq!(app.focused_panel, 1);
    app.focused_panel = (app.focused_panel + 1) % 5;
    assert_eq!(app.focused_panel, 2); // replace panel
    app.focused_panel = (app.focused_panel + 1) % 5;
    assert_eq!(app.focused_panel, 3); // matches
    app.focused_panel = (app.focused_panel + 1) % 5;
    assert_eq!(app.focused_panel, 4); // explanation
    app.focused_panel = (app.focused_panel + 1) % 5;
    assert_eq!(app.focused_panel, 0); // back to regex
}

#[test]
fn replacement_with_named_groups() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("2024-01");
    app.set_pattern(r"(?P<y>\d{4})-(?P<m>\d{2})");
    app.set_replacement("${m}/${y}");

    assert!(app.replace_result.is_some());
    let result = app.replace_result.as_ref().unwrap();
    assert_eq!(result.output, "01/2024");
}

#[test]
fn replacement_clears_on_empty_template() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("hello 123");
    app.set_pattern(r"\d+");
    app.set_replacement("[NUM]");
    assert!(app.replace_result.is_some());

    app.set_replacement("");
    assert!(app.replace_result.is_none());
}

// --- Phase 2 tests ---

#[test]
fn undo_redo_regex_editor() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("hello 123");
    app.regex_editor.insert_char('\\');
    app.regex_editor.insert_char('d');
    app.regex_editor.insert_char('+');
    app.recompute();
    assert_eq!(app.matches.len(), 1);

    app.regex_editor.undo();
    app.recompute();
    // Pattern is now "\d" — still matches
    assert_eq!(app.regex_editor.content(), "\\d");

    app.regex_editor.redo();
    app.recompute();
    assert_eq!(app.regex_editor.content(), "\\d+");
}

#[test]
fn pattern_history_navigation() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("abc 123 def 456");

    // Commit first pattern
    app.set_pattern(r"\d+");
    app.commit_pattern_to_history();

    // Commit second pattern
    app.set_pattern(r"\w+");
    app.commit_pattern_to_history();

    assert_eq!(app.history.entries.len(), 2);

    // Navigate back
    app.set_pattern("current");
    app.history_prev();
    assert_eq!(app.regex_editor.content(), r"\w+");

    app.history_prev();
    assert_eq!(app.regex_editor.content(), r"\d+");

    // Navigate forward
    app.history_next();
    assert_eq!(app.regex_editor.content(), r"\w+");

    // Past end restores temp
    app.history_next();
    assert_eq!(app.regex_editor.content(), "current");
}

#[test]
fn pattern_history_dedup() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_pattern(r"\d+");
    app.commit_pattern_to_history();
    app.commit_pattern_to_history(); // same pattern, should dedup
    assert_eq!(app.history.entries.len(), 1);
}

#[test]
fn match_selection_navigation() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("aa bb cc");
    app.set_pattern(r"\w+");
    assert_eq!(app.matches.len(), 3);

    // Start at match 0, no capture
    assert_eq!(app.selection.match_index, 0);
    assert_eq!(app.selection.capture_index, None);

    // Navigate down through matches (no captures here)
    app.select_match_next();
    assert_eq!(app.selection.match_index, 1);

    app.select_match_next();
    assert_eq!(app.selection.match_index, 2);

    // Can't go past last match
    app.select_match_next();
    assert_eq!(app.selection.match_index, 2);

    // Navigate back up
    app.select_match_prev();
    assert_eq!(app.selection.match_index, 1);

    app.select_match_prev();
    assert_eq!(app.selection.match_index, 0);

    // Can't go before first match
    app.select_match_prev();
    assert_eq!(app.selection.match_index, 0);
}

#[test]
fn match_selection_with_captures() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("user@example");
    app.set_pattern(r"(\w+)@(\w+)");
    assert_eq!(app.matches.len(), 1);
    assert_eq!(app.matches[0].captures.len(), 2);

    // Start at match 0
    assert_eq!(app.selection.match_index, 0);
    assert_eq!(app.selection.capture_index, None);

    // Next goes to first capture
    app.select_match_next();
    assert_eq!(app.selection.match_index, 0);
    assert_eq!(app.selection.capture_index, Some(0));

    // Next goes to second capture
    app.select_match_next();
    assert_eq!(app.selection.match_index, 0);
    assert_eq!(app.selection.capture_index, Some(1));

    // Prev goes back to first capture
    app.select_match_prev();
    assert_eq!(app.selection.capture_index, Some(0));

    // Prev goes back to match header
    app.select_match_prev();
    assert_eq!(app.selection.capture_index, None);
}

#[test]
fn selection_resets_on_rematch() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("aa bb");
    app.set_pattern(r"\w+");
    app.select_match_next();
    assert_eq!(app.selection.match_index, 1);

    // Changing test string resets selection
    app.set_test_string("aa bb cc");
    assert_eq!(app.selection.match_index, 0);
    assert_eq!(app.selection.capture_index, None);
}

#[test]
fn help_pages_render() {
    let mut terminal = create_test_terminal();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.overlay.help = true;

    // Page 0 (default)
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();

    // Page 1
    app.overlay.help_page = 1;
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();

    // Page 2
    app.overlay.help_page = 2;
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
}

#[test]
fn help_page_clamped() {
    let mut terminal = create_test_terminal();
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.overlay.help = true;
    app.overlay.help_page = 99; // out of bounds, should clamp
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
}

#[test]
fn multiline_flag_matching() {
    let mut app = App::new(
        EngineKind::RustRegex,
        EngineFlags {
            multi_line: true,
            ..Default::default()
        },
    );
    app.set_test_string("line1\nno match\nline42");
    app.set_pattern(r"^line\d+$");
    assert_eq!(app.matches.len(), 2);
    assert_eq!(app.matches[0].text, "line1");
    assert_eq!(app.matches[1].text, "line42");
}

#[test]
fn dotall_flag_matching() {
    let mut app = App::new(
        EngineKind::RustRegex,
        EngineFlags {
            dot_matches_newline: true,
            ..Default::default()
        },
    );
    app.set_test_string("a\nb");
    app.set_pattern("a.b");
    assert_eq!(app.matches.len(), 1);
    assert_eq!(app.matches[0].text, "a\nb");
}

#[test]
fn whitespace_visualization_toggle() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    assert!(!app.show_whitespace);
    app.show_whitespace = true;
    assert!(app.show_whitespace);

    // Rendering with whitespace mode should not panic
    let mut terminal = create_test_terminal();
    app.set_test_string("hello world\nfoo bar");
    app.set_pattern(r"\w+");
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
}

#[test]
fn compute_layout_does_not_panic() {
    use ratatui::layout::Rect;
    for show_quickref in [false, true] {
        // Wide terminal
        let _ = ui::compute_layout(Rect::new(0, 0, 120, 40), show_quickref);
        // Narrow terminal
        let _ = ui::compute_layout(Rect::new(0, 0, 40, 20), show_quickref);
        // Tiny terminal
        let _ = ui::compute_layout(Rect::new(0, 0, 10, 10), show_quickref);
    }
}

#[test]
fn quickref_side_panel_appears_only_when_wide_and_toggled() {
    use ratatui::layout::Rect;
    // Wide + on: quickref is Some
    let wide_on = ui::compute_layout(Rect::new(0, 0, 200, 40), true);
    assert!(wide_on.quickref.is_some());
    // Wide + off: quickref is None
    let wide_off = ui::compute_layout(Rect::new(0, 0, 200, 40), false);
    assert!(wide_off.quickref.is_none());
    // Narrow + on: quickref is suppressed to keep the results area usable
    let narrow_on = ui::compute_layout(Rect::new(0, 0, 60, 30), true);
    assert!(narrow_on.quickref.is_none());
}

#[test]
fn quickref_side_panel_spans_full_height_above_status_bar() {
    use ratatui::layout::Rect;
    // Reported in #83 follow-up: zoomed-in terminals clipped most of the
    // quickref content because the panel was confined to the results area.
    // The panel now carves a strip from the full main area (everything
    // above the 1-row status bar) so it can scroll its content freely.
    let layout = ui::compute_layout(Rect::new(0, 0, 200, 40), true);
    let qr = layout
        .quickref
        .expect("quickref should be Some on a wide+on layout");
    assert_eq!(
        qr.height, 39,
        "quickref should span terminal height minus the 1-row status bar"
    );
    assert_eq!(layout.status_bar.height, 1);
}

#[test]
fn test_empty_state_render() {
    let mut terminal = create_test_terminal();
    let app = App::new(EngineKind::RustRegex, EngineFlags::default());
    // No pattern, no test string — should render without panic
    assert!(app.regex_editor.content().is_empty());
    assert!(app.test_editor.content().is_empty());
    assert!(app.matches.is_empty());
    assert!(app.error.is_none());
    terminal.draw(|frame| ui::render(frame, &app)).unwrap();
}

#[test]
fn test_replace_invalid_capture_ref() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.set_test_string("hello world");
    app.set_pattern(r"(\w+) (\w+)");
    // $99 references a non-existent group — should not panic
    app.set_replacement("$99");
    assert!(app.replace_result.is_some());
    let result = app.replace_result.as_ref().unwrap();
    // $99 is parsed as $9 then literal '9', $9 doesn't exist so nothing
    // The output should just not crash
    assert!(!result.output.is_empty() || result.output.is_empty());
}

#[test]
fn upper_scroll_bound() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.overlay.help = true;
    app.help_scroll_offset = 0;
    let before = app.help_scroll_offset;
    handle_help_key(&mut app, press_key(KeyCode::Up));
    assert_eq!(app.help_scroll_offset, before);
}

#[test]
fn scroll_up_decrements_offset() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.overlay.help = true;
    app.help_scroll_offset = 5;
    handle_help_key(&mut app, press_key(KeyCode::Up));
    assert_eq!(app.help_scroll_offset, 4);
}

#[test]
fn scroll_lower_bound() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());

    app.overlay.help = true;
    let bound_checker = |app: &mut App| {
        let max_scroll = app.help_page_max_scroll();
        app.help_scroll_offset = max_scroll;

        // Test Lower Bound: Press 'j' at max index => should clip at max_scroll
        // Simulate pressing 3 times beyond the max_scroll
        for _ in 0..3 {
            handle_help_key(app, press_key(KeyCode::Down));
        }
        assert_eq!(
            app.help_scroll_offset, max_scroll,
            "Scroll offset should be bounded by max_scroll"
        );
    };

    // Page 0
    bound_checker(&mut app);

    // Page 1
    handle_help_key(&mut app, press_key(KeyCode::Char('l')));
    bound_checker(&mut app);

    // Page 2
    handle_help_key(&mut app, press_key(KeyCode::Char('l')));
    bound_checker(&mut app);
}

#[test]
fn scroll_offset_reset() {
    let mut app = App::new(EngineKind::RustRegex, EngineFlags::default());
    app.help_scroll_offset = 5;
    handle_help_key(&mut app, press_key(KeyCode::Left));
    assert_eq!(
        app.help_scroll_offset, 0,
        "Scroll offset does not reset on page change"
    );
    app.help_scroll_offset = 5;
    handle_help_key(&mut app, press_key(KeyCode::Esc));
    assert_eq!(
        app.help_scroll_offset, 0,
        "Scroll offset does not reset on close"
    );
}

#[test]
fn scroll_help_pages_length_check() {
    let length_hashmap = build_lengths_of_help_pages();
    // For now it only checks all EngineKinds inside ::all()
    for engine in EngineKind::all() {
        let page_lengths_of_engine = length_hashmap.get(&engine);
        match page_lengths_of_engine {
            Some(vec) => {
                assert!(
                    vec.iter().all(|&x| x > crate::ui::HELP_PAGE_PADDING),
                    "Page has no content for the engine {engine:?}"
                );
            }
            None => {
                panic!("Engine {engine:?} was not found in the page lengths hashmap");
            }
        }
    }
}
