//! Tests for LoreApp.

#![cfg(test)]

use chrono::TimeDelta;
use ftui::{Cmd, Event, KeyCode, KeyEvent, Model, Modifiers};

use crate::clock::FakeClock;
use crate::message::{InputMode, Msg, Screen};

use super::LoreApp;

fn test_app() -> LoreApp {
    LoreApp::with_clock(Box::new(FakeClock::new(chrono::Utc::now())))
}

/// Verify that `App::fullscreen(LoreApp::new()).run()` compiles.
fn _assert_app_fullscreen_compiles() {
    fn _inner() {
        use ftui::App;
        let _app_builder = App::fullscreen(LoreApp::new());
    }
}

/// Verify that `App::inline(LoreApp::new(), 12).run()` compiles.
fn _assert_app_inline_compiles() {
    fn _inner() {
        use ftui::App;
        let _app_builder = App::inline(LoreApp::new(), 12);
    }
}

#[test]
fn test_lore_app_init_returns_none() {
    let mut app = test_app();
    let cmd = app.init();
    assert!(matches!(cmd, Cmd::None));
}

#[test]
fn test_lore_app_quit_returns_quit_cmd() {
    let mut app = test_app();
    let cmd = app.update(Msg::Quit);
    assert!(matches!(cmd, Cmd::Quit));
}

#[test]
fn test_lore_app_tick_returns_none() {
    let mut app = test_app();
    let cmd = app.update(Msg::Tick);
    assert!(matches!(cmd, Cmd::None));
}

#[test]
fn test_lore_app_navigate_to_updates_nav_stack() {
    let mut app = test_app();
    let cmd = app.update(Msg::NavigateTo(Screen::IssueList));
    assert!(matches!(cmd, Cmd::None));
    assert!(app.navigation.is_at(&Screen::IssueList));
    assert_eq!(app.navigation.depth(), 2);
}

#[test]
fn test_lore_app_go_back() {
    let mut app = test_app();
    app.update(Msg::NavigateTo(Screen::IssueList));
    app.update(Msg::GoBack);
    assert!(app.navigation.is_at(&Screen::Dashboard));
}

#[test]
fn test_lore_app_go_forward() {
    let mut app = test_app();
    app.update(Msg::NavigateTo(Screen::IssueList));
    app.update(Msg::GoBack);
    app.update(Msg::GoForward);
    assert!(app.navigation.is_at(&Screen::IssueList));
}

#[test]
fn test_ctrl_c_always_quits() {
    let mut app = test_app();
    let key = KeyEvent::new(KeyCode::Char('c')).with_modifiers(Modifiers::CTRL);
    let cmd = app.update(Msg::RawEvent(Event::Key(key)));
    assert!(matches!(cmd, Cmd::Quit));
}

#[test]
fn test_q_key_quits_in_normal_mode() {
    let mut app = test_app();
    let key = KeyEvent::new(KeyCode::Char('q'));
    let cmd = app.update(Msg::RawEvent(Event::Key(key)));
    assert!(matches!(cmd, Cmd::Quit));
}

#[test]
fn test_q_key_blocked_in_text_mode() {
    let mut app = test_app();
    app.input_mode = InputMode::Text;
    let key = KeyEvent::new(KeyCode::Char('q'));
    let cmd = app.update(Msg::RawEvent(Event::Key(key)));
    // q in text mode should NOT quit.
    assert!(matches!(cmd, Cmd::None));
}

#[test]
fn test_esc_blurs_text_mode() {
    let mut app = test_app();
    app.input_mode = InputMode::Text;
    app.state.search.query_focused = true;

    let key = KeyEvent::new(KeyCode::Escape);
    app.update(Msg::RawEvent(Event::Key(key)));

    assert!(matches!(app.input_mode, InputMode::Normal));
    assert!(!app.state.has_text_focus());
}

#[test]
fn test_g_prefix_enters_go_mode() {
    let mut app = test_app();
    let key = KeyEvent::new(KeyCode::Char('g'));
    app.update(Msg::RawEvent(Event::Key(key)));
    assert!(matches!(app.input_mode, InputMode::GoPrefix { .. }));
}

#[test]
fn test_g_then_i_navigates_to_issues() {
    let mut app = test_app();

    // First key: 'g'
    let key_g = KeyEvent::new(KeyCode::Char('g'));
    app.update(Msg::RawEvent(Event::Key(key_g)));

    // Second key: 'i'
    let key_i = KeyEvent::new(KeyCode::Char('i'));
    app.update(Msg::RawEvent(Event::Key(key_i)));

    assert!(app.navigation.is_at(&Screen::IssueList));
}

#[test]
fn test_g_then_s_on_bootstrap_starts_sync_in_place() {
    let mut app = test_app();
    app.update(Msg::NavigateTo(Screen::Bootstrap));

    // First key: 'g'
    let key_g = KeyEvent::new(KeyCode::Char('g'));
    app.update(Msg::RawEvent(Event::Key(key_g)));

    // Second key: 's'
    let key_s = KeyEvent::new(KeyCode::Char('s'));
    app.update(Msg::RawEvent(Event::Key(key_s)));

    assert!(app.navigation.is_at(&Screen::Bootstrap));
    assert!(app.state.bootstrap.sync_started);
    assert!(matches!(app.input_mode, InputMode::Normal));
}

#[test]
fn test_g_then_s_from_dashboard_navigates_to_sync_screen() {
    let mut app = test_app();

    // First key: 'g'
    let key_g = KeyEvent::new(KeyCode::Char('g'));
    app.update(Msg::RawEvent(Event::Key(key_g)));

    // Second key: 's'
    let key_s = KeyEvent::new(KeyCode::Char('s'));
    app.update(Msg::RawEvent(Event::Key(key_s)));

    assert!(app.navigation.is_at(&Screen::Sync));
}

#[test]
fn test_go_prefix_timeout_cancels() {
    let clock = FakeClock::new(chrono::Utc::now());
    let mut app = LoreApp::with_clock(Box::new(clock.clone()));

    // Press 'g'.
    let key_g = KeyEvent::new(KeyCode::Char('g'));
    app.update(Msg::RawEvent(Event::Key(key_g)));
    assert!(matches!(app.input_mode, InputMode::GoPrefix { .. }));

    // Advance clock past timeout.
    clock.advance(TimeDelta::milliseconds(600));

    // Press 'i' after timeout — should NOT navigate to issues.
    let key_i = KeyEvent::new(KeyCode::Char('i'));
    app.update(Msg::RawEvent(Event::Key(key_i)));

    // Should still be at Dashboard (no navigation happened).
    assert!(app.navigation.is_at(&Screen::Dashboard));
    assert!(matches!(app.input_mode, InputMode::Normal));
}

#[test]
fn test_show_help_toggles() {
    let mut app = test_app();
    assert!(!app.state.show_help);

    app.update(Msg::ShowHelp);
    assert!(app.state.show_help);

    app.update(Msg::ShowHelp);
    assert!(!app.state.show_help);
}

#[test]
fn test_error_msg_sets_toast() {
    let mut app = test_app();
    app.update(Msg::Error(crate::message::AppError::DbBusy));
    assert!(app.state.error_toast.is_some());
    assert!(app.state.error_toast.as_ref().unwrap().contains("busy"));
}

#[test]
fn test_resize_updates_terminal_size() {
    let mut app = test_app();
    app.update(Msg::Resize {
        width: 120,
        height: 40,
    });
    assert_eq!(app.state.terminal_size, (120, 40));
}

#[test]
fn test_stale_result_dropped() {
    use crate::message::Screen;
    use crate::task_supervisor::TaskKey;

    let mut app = test_app();

    // Submit two tasks for IssueList — second supersedes first.
    let gen1 = app
        .supervisor
        .submit(TaskKey::LoadScreen(Screen::IssueList))
        .generation;
    let gen2 = app
        .supervisor
        .submit(TaskKey::LoadScreen(Screen::IssueList))
        .generation;

    // Stale result with gen1 should be ignored.
    app.update(Msg::IssueListLoaded {
        generation: gen1,
        page: crate::state::issue_list::IssueListPage {
            rows: vec![crate::state::issue_list::IssueListRow {
                project_path: "group/project".into(),
                iid: 1,
                title: "stale".into(),
                state: "opened".into(),
                author: "taylor".into(),
                labels: vec![],
                updated_at: 1_700_000_000_000,
            }],
            next_cursor: None,
            total_count: 1,
        },
    });
    assert!(app.state.issue_list.rows.is_empty());

    // Current result with gen2 should be applied.
    app.update(Msg::IssueListLoaded {
        generation: gen2,
        page: crate::state::issue_list::IssueListPage {
            rows: vec![crate::state::issue_list::IssueListRow {
                project_path: "group/project".into(),
                iid: 2,
                title: "fresh".into(),
                state: "opened".into(),
                author: "taylor".into(),
                labels: vec![],
                updated_at: 1_700_000_000_000,
            }],
            next_cursor: None,
            total_count: 1,
        },
    });
    assert_eq!(app.state.issue_list.rows.len(), 1);
    assert_eq!(app.state.issue_list.rows[0].title, "fresh");
}

#[test]
fn test_crash_context_records_events() {
    let mut app = test_app();
    app.update(Msg::Tick);
    app.update(Msg::NavigateTo(Screen::IssueList));

    // Should have recorded at least 2 events.
    assert!(app.crash_context.len() >= 2);
}

#[test]
fn test_navigate_sets_loading_initial_on_first_visit() {
    use crate::state::LoadState;

    let mut app = test_app();
    app.update(Msg::NavigateTo(Screen::IssueList));
    // First visit should show full-screen spinner (LoadingInitial).
    assert_eq!(
        *app.state.load_state.get(&Screen::IssueList),
        LoadState::LoadingInitial
    );
}

#[test]
fn test_navigate_sets_refreshing_on_revisit() {
    use crate::state::LoadState;

    let mut app = test_app();
    // First visit → LoadingInitial.
    app.update(Msg::NavigateTo(Screen::IssueList));
    // Simulate load completing.
    app.state.set_loading(Screen::IssueList, LoadState::Idle);
    // Go back, then revisit.
    app.update(Msg::GoBack);
    app.update(Msg::NavigateTo(Screen::IssueList));
    // Second visit should show corner spinner (Refreshing).
    assert_eq!(
        *app.state.load_state.get(&Screen::IssueList),
        LoadState::Refreshing
    );
}

#[test]
fn test_command_palette_opens_from_ctrl_p() {
    let mut app = test_app();
    let key = KeyEvent::new(KeyCode::Char('p')).with_modifiers(Modifiers::CTRL);
    app.update(Msg::RawEvent(Event::Key(key)));
    assert!(matches!(app.input_mode, InputMode::Palette));
    assert!(app.state.command_palette.query_focused);
}

#[test]
fn test_esc_closes_palette() {
    let mut app = test_app();
    app.input_mode = InputMode::Palette;

    let key = KeyEvent::new(KeyCode::Escape);
    app.update(Msg::RawEvent(Event::Key(key)));

    assert!(matches!(app.input_mode, InputMode::Normal));
}

#[test]
fn test_blur_text_input_msg() {
    let mut app = test_app();
    app.input_mode = InputMode::Text;
    app.state.search.query_focused = true;

    app.update(Msg::BlurTextInput);

    assert!(matches!(app.input_mode, InputMode::Normal));
    assert!(!app.state.has_text_focus());
}

#[test]
fn test_default_is_new() {
    let app = LoreApp::default();
    assert!(app.navigation.is_at(&Screen::Dashboard));
    assert!(matches!(app.input_mode, InputMode::Normal));
}

#[test]
fn test_sync_completed_from_bootstrap_resets_navigation_and_state() {
    let mut app = test_app();

    app.update(Msg::NavigateTo(Screen::Bootstrap));
    app.update(Msg::SyncStarted);
    assert!(app.state.bootstrap.sync_started);
    assert!(app.navigation.is_at(&Screen::Bootstrap));

    app.update(Msg::SyncCompleted { elapsed_ms: 1234 });

    assert!(app.navigation.is_at(&Screen::Dashboard));
    assert_eq!(app.navigation.depth(), 1);
    assert!(!app.state.bootstrap.sync_started);
}
