//! Vertical slice integration tests for TUI Phase 2.
//!
//! Validates that core screens work together end-to-end with synthetic
//! data flows, navigation preserves state, stale results are dropped,
//! and input mode is always recoverable.

use ftui::render::frame::Frame;
use ftui::render::grapheme_pool::GraphemePool;
use ftui::{Cmd, Event, KeyCode, KeyEvent, Model, Modifiers};

use lore_tui::app::LoreApp;
use lore_tui::clock::FakeClock;
use lore_tui::message::{EntityKey, InputMode, Msg, Screen};
use lore_tui::state::dashboard::{DashboardData, EntityCounts, LastSyncInfo};
use lore_tui::state::issue_detail::{IssueDetailData, IssueMetadata};
use lore_tui::state::issue_list::{IssueListPage, IssueListRow};
use lore_tui::state::mr_detail::MrDetailData;
use lore_tui::state::mr_list::{MrListPage, MrListRow};
use lore_tui::task_supervisor::TaskKey;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn test_app() -> LoreApp {
    let mut app = LoreApp::new();
    app.clock = Box::new(FakeClock::new(chrono::Utc::now()));
    app
}

fn synthetic_dashboard_data() -> DashboardData {
    DashboardData {
        counts: EntityCounts {
            issues_total: 10,
            issues_open: 5,
            mrs_total: 8,
            mrs_open: 3,
            discussions: 15,
            notes_total: 50,
            notes_system_pct: 20,
            documents: 20,
            embeddings: 100,
        },
        projects: vec![],
        recent: vec![],
        last_sync: Some(LastSyncInfo {
            status: "succeeded".into(),
            finished_at: Some(1_700_000_000_000),
            command: "sync".into(),
            error: None,
        }),
    }
}

fn synthetic_issue_list_page() -> IssueListPage {
    IssueListPage {
        rows: vec![
            IssueListRow {
                project_path: "group/project".into(),
                iid: 1,
                title: "First issue".into(),
                state: "opened".into(),
                author: "alice".into(),
                labels: vec!["backend".into()],
                updated_at: 1_700_000_000_000,
            },
            IssueListRow {
                project_path: "group/project".into(),
                iid: 2,
                title: "Second issue".into(),
                state: "closed".into(),
                author: "bob".into(),
                labels: vec![],
                updated_at: 1_700_000_010_000,
            },
        ],
        next_cursor: None,
        total_count: 2,
    }
}

fn synthetic_issue_detail() -> IssueDetailData {
    IssueDetailData {
        metadata: IssueMetadata {
            iid: 1,
            project_path: "group/project".into(),
            title: "First issue".into(),
            description: "Test description".into(),
            state: "opened".into(),
            author: "alice".into(),
            assignees: vec!["bob".into()],
            labels: vec!["backend".into()],
            milestone: None,
            due_date: None,
            created_at: 1_700_000_000_000,
            updated_at: 1_700_000_060_000,
            web_url: "https://gitlab.com/group/project/-/issues/1".into(),
            discussion_count: 2,
        },
        cross_refs: vec![],
    }
}

fn synthetic_mr_list_page() -> MrListPage {
    MrListPage {
        rows: vec![MrListRow {
            project_path: "group/project".into(),
            iid: 10,
            title: "Fix auth".into(),
            state: "opened".into(),
            author: "alice".into(),
            labels: vec![],
            updated_at: 1_700_000_000_000,
            draft: false,
            target_branch: "main".into(),
        }],
        next_cursor: None,
        total_count: 1,
    }
}

fn synthetic_mr_detail() -> MrDetailData {
    MrDetailData {
        metadata: lore_tui::state::mr_detail::MrMetadata {
            iid: 10,
            project_path: "group/project".into(),
            title: "Fix auth".into(),
            description: "MR description".into(),
            state: "opened".into(),
            draft: false,
            author: "alice".into(),
            assignees: vec!["bob".into()],
            reviewers: vec!["carol".into()],
            labels: vec![],
            source_branch: "fix-auth".into(),
            target_branch: "main".into(),
            merge_status: "mergeable".into(),
            created_at: 1_700_000_000_000,
            updated_at: 1_700_000_060_000,
            merged_at: None,
            web_url: "https://gitlab.com/group/project/-/merge_requests/10".into(),
            discussion_count: 1,
            file_change_count: 2,
        },
        cross_refs: vec![],
        file_changes: vec![],
    }
}

/// Inject dashboard data with matching generation.
fn load_dashboard(app: &mut LoreApp) {
    let generation = app
        .supervisor
        .submit(TaskKey::LoadScreen(Screen::Dashboard))
        .generation;
    app.update(Msg::DashboardLoaded {
        generation,
        data: Box::new(synthetic_dashboard_data()),
    });
}

/// Navigate to issue list and inject data.
fn navigate_and_load_issue_list(app: &mut LoreApp) {
    app.update(Msg::NavigateTo(Screen::IssueList));
    let generation = app
        .supervisor
        .submit(TaskKey::LoadScreen(Screen::IssueList))
        .generation;
    app.update(Msg::IssueListLoaded {
        generation,
        page: synthetic_issue_list_page(),
    });
}

/// Navigate to issue detail and inject data.
fn navigate_and_load_issue_detail(app: &mut LoreApp, key: EntityKey) {
    let screen = Screen::IssueDetail(key.clone());
    app.update(Msg::NavigateTo(screen.clone()));
    let generation = app
        .supervisor
        .submit(TaskKey::LoadScreen(screen))
        .generation;
    app.update(Msg::IssueDetailLoaded {
        generation,
        key,
        data: Box::new(synthetic_issue_detail()),
    });
}

// ---------------------------------------------------------------------------
// Nav flow tests
// ---------------------------------------------------------------------------

/// TDD anchor: Dashboard -> IssueList -> IssueDetail -> Esc -> IssueList,
/// verifies cursor position is preserved on back-navigation.
#[test]
fn test_dashboard_to_issue_detail_roundtrip() {
    let mut app = test_app();
    assert!(app.navigation.is_at(&Screen::Dashboard));

    // Navigate to IssueList and load data.
    navigate_and_load_issue_list(&mut app);
    assert!(app.navigation.is_at(&Screen::IssueList));
    assert_eq!(app.state.issue_list.rows.len(), 2);

    // Navigate to IssueDetail for issue #1.
    let issue_key = EntityKey::issue(1, 1);
    navigate_and_load_issue_detail(&mut app, issue_key);
    assert!(matches!(app.navigation.current(), Screen::IssueDetail(_)));

    // Go back — should return to IssueList with data preserved.
    app.update(Msg::GoBack);
    assert!(app.navigation.is_at(&Screen::IssueList));
    // Data should still be there (state preserved on navigation).
    assert_eq!(app.state.issue_list.rows.len(), 2);
}

/// Navigate Dashboard -> IssueList -> MrList -> MrDetail -> Home.
#[test]
fn test_full_nav_flow_home() {
    let mut app = test_app();

    // Issue list.
    navigate_and_load_issue_list(&mut app);
    assert!(app.navigation.is_at(&Screen::IssueList));

    // MR list.
    app.update(Msg::NavigateTo(Screen::MrList));
    let generation = app
        .supervisor
        .submit(TaskKey::LoadScreen(Screen::MrList))
        .generation;
    app.update(Msg::MrListLoaded {
        generation,
        page: synthetic_mr_list_page(),
    });
    assert!(app.navigation.is_at(&Screen::MrList));

    // MR detail.
    let mr_key = EntityKey::mr(1, 10);
    let mr_screen = Screen::MrDetail(mr_key.clone());
    app.update(Msg::NavigateTo(mr_screen.clone()));
    let generation = app
        .supervisor
        .submit(TaskKey::LoadScreen(mr_screen))
        .generation;
    app.update(Msg::MrDetailLoaded {
        generation,
        key: mr_key,
        data: Box::new(synthetic_mr_detail()),
    });
    assert!(matches!(app.navigation.current(), Screen::MrDetail(_)));

    // Go home.
    app.update(Msg::GoHome);
    assert!(app.navigation.is_at(&Screen::Dashboard));
}

/// Verify back-navigation preserves issue list data and MR list data.
#[test]
fn test_state_preserved_on_back_navigation() {
    let mut app = test_app();

    // Load issue list.
    navigate_and_load_issue_list(&mut app);
    assert_eq!(app.state.issue_list.rows.len(), 2);

    // Navigate to MR list.
    app.update(Msg::NavigateTo(Screen::MrList));
    let generation = app
        .supervisor
        .submit(TaskKey::LoadScreen(Screen::MrList))
        .generation;
    app.update(Msg::MrListLoaded {
        generation,
        page: synthetic_mr_list_page(),
    });

    // Both states should be populated.
    assert_eq!(app.state.issue_list.rows.len(), 2);
    assert_eq!(app.state.mr_list.rows.len(), 1);

    // Go back to issue list — data should still be there.
    app.update(Msg::GoBack);
    assert!(app.navigation.is_at(&Screen::IssueList));
    assert_eq!(app.state.issue_list.rows.len(), 2);
}

// ---------------------------------------------------------------------------
// Stale result guard
// ---------------------------------------------------------------------------

/// Rapidly navigate between screens, injecting out-of-order results.
/// Stale results should be silently dropped.
#[test]
fn test_stale_result_guard_rapid_navigation() {
    let mut app = test_app();

    // Navigate to IssueList, capturing generation.
    app.update(Msg::NavigateTo(Screen::IssueList));
    let generation1 = app
        .supervisor
        .submit(TaskKey::LoadScreen(Screen::IssueList))
        .generation;

    // Quickly navigate away and back — new generation.
    app.update(Msg::GoBack);
    app.update(Msg::NavigateTo(Screen::IssueList));
    let generation2 = app
        .supervisor
        .submit(TaskKey::LoadScreen(Screen::IssueList))
        .generation;

    // Late arrival of generation1 — should be dropped.
    app.update(Msg::IssueListLoaded {
        generation: generation1,
        page: IssueListPage {
            rows: vec![IssueListRow {
                project_path: "g/p".into(),
                iid: 999,
                title: "stale".into(),
                state: "opened".into(),
                author: "x".into(),
                labels: vec![],
                updated_at: 0,
            }],
            next_cursor: None,
            total_count: 1,
        },
    });
    assert!(
        app.state.issue_list.rows.is_empty(),
        "stale result should be dropped"
    );

    // generation2 should be accepted.
    app.update(Msg::IssueListLoaded {
        generation: generation2,
        page: synthetic_issue_list_page(),
    });
    assert_eq!(app.state.issue_list.rows.len(), 2);
    assert_eq!(app.state.issue_list.rows[0].title, "First issue");
}

// ---------------------------------------------------------------------------
// Input mode fuzz (stuck-input check)
// ---------------------------------------------------------------------------

/// Fuzz 1000 random key sequences and verify:
/// 1. No panics
/// 2. InputMode is always recoverable via Esc + Ctrl+C
/// 3. Final state is consistent
#[test]
fn test_input_mode_fuzz_no_stuck_state() {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    let mut app = test_app();

    // Deterministic pseudo-random key generation.
    let keys = [
        KeyCode::Char('g'),
        KeyCode::Char('i'),
        KeyCode::Char('m'),
        KeyCode::Char('h'),
        KeyCode::Char('s'),
        KeyCode::Char('q'),
        KeyCode::Char('p'),
        KeyCode::Char('/'),
        KeyCode::Char('?'),
        KeyCode::Tab,
        KeyCode::BackTab,
        KeyCode::Escape,
        KeyCode::Enter,
        KeyCode::Up,
        KeyCode::Down,
        KeyCode::Left,
        KeyCode::Right,
        KeyCode::Home,
        KeyCode::End,
    ];

    let modifiers_set = [
        Modifiers::NONE,
        Modifiers::SHIFT,
        Modifiers::CTRL,
        Modifiers::NONE,
        Modifiers::NONE,
    ];

    // Run 1000 random key events.
    for i in 0..1000_u64 {
        // Simple deterministic hash to pick key + modifier.
        let mut hasher = DefaultHasher::new();
        i.hash(&mut hasher);
        let h = hasher.finish();

        let key_code = keys[(h as usize) % keys.len()];
        let mods = modifiers_set[((h >> 16) as usize) % modifiers_set.len()];

        // Skip Ctrl+C (would quit) and 'q' in normal mode (would quit).
        if key_code == KeyCode::Char('c') && mods.contains(Modifiers::CTRL) {
            continue;
        }
        if key_code == KeyCode::Char('q') && mods == Modifiers::NONE {
            // Only skip if in Normal mode to avoid quitting the test.
            if matches!(app.input_mode, InputMode::Normal) {
                continue;
            }
        }

        let key_event = if mods == Modifiers::NONE {
            KeyEvent::new(key_code)
        } else {
            KeyEvent::new(key_code).with_modifiers(mods)
        };

        let cmd = app.update(Msg::RawEvent(Event::Key(key_event)));
        // Should never produce Quit from our filtered set (we skip q and Ctrl+C).
        if matches!(cmd, Cmd::Quit) {
            // This can happen from 'q' in non-Normal modes where we didn't filter.
            // Recreate app to continue fuzzing.
            app = test_app();
        }
    }

    // Recovery check: Esc should always bring us back to Normal mode.
    app.update(Msg::RawEvent(Event::Key(KeyEvent::new(KeyCode::Escape))));
    // After Esc, we should be in Normal mode (or if already Normal, stay there).
    // GoPrefix → Normal, Text → Normal, Palette → Normal.
    assert!(
        matches!(app.input_mode, InputMode::Normal),
        "Esc should always recover to Normal mode, got: {:?}",
        app.input_mode
    );

    // Ctrl+C should always quit.
    let ctrl_c = KeyEvent::new(KeyCode::Char('c')).with_modifiers(Modifiers::CTRL);
    let cmd = app.update(Msg::RawEvent(Event::Key(ctrl_c)));
    assert!(matches!(cmd, Cmd::Quit));
}

// ---------------------------------------------------------------------------
// Bootstrap → Dashboard transition
// ---------------------------------------------------------------------------

/// Bootstrap screen should auto-transition to Dashboard when sync completes.
#[test]
fn test_bootstrap_to_dashboard_after_sync() {
    let mut app = test_app();

    // Start on Bootstrap screen.
    app.update(Msg::NavigateTo(Screen::Bootstrap));
    assert!(app.navigation.is_at(&Screen::Bootstrap));
    assert!(!app.state.bootstrap.sync_started);

    // User starts sync via key path (g then s).
    app.update(Msg::RawEvent(Event::Key(KeyEvent::new(KeyCode::Char('g')))));
    app.update(Msg::RawEvent(Event::Key(KeyEvent::new(KeyCode::Char('s')))));
    assert!(app.state.bootstrap.sync_started);

    // Sync completes — should auto-transition to Dashboard.
    app.update(Msg::SyncCompleted { elapsed_ms: 5000 });
    assert!(
        app.navigation.is_at(&Screen::Dashboard),
        "Should auto-transition to Dashboard after sync completes on Bootstrap"
    );
}

/// SyncCompleted on non-Bootstrap screen should NOT navigate.
#[test]
fn test_sync_completed_does_not_navigate_from_other_screens() {
    let mut app = test_app();

    // Navigate to IssueList.
    app.update(Msg::NavigateTo(Screen::IssueList));
    assert!(app.navigation.is_at(&Screen::IssueList));

    // SyncCompleted should be a no-op.
    app.update(Msg::SyncCompleted { elapsed_ms: 3000 });
    assert!(
        app.navigation.is_at(&Screen::IssueList),
        "SyncCompleted should not navigate when not on Bootstrap"
    );
}

// ---------------------------------------------------------------------------
// Render all screens (no-panic check)
// ---------------------------------------------------------------------------

/// Render every screen variant to verify no panics with synthetic data.
#[test]
fn test_render_all_screens_no_panic() {
    let mut pool = GraphemePool::new();

    // Load data for all screens.
    let mut app = test_app();
    load_dashboard(&mut app);
    navigate_and_load_issue_list(&mut app);
    app.update(Msg::GoBack);

    // Load MR list.
    app.update(Msg::NavigateTo(Screen::MrList));
    let generation = app
        .supervisor
        .submit(TaskKey::LoadScreen(Screen::MrList))
        .generation;
    app.update(Msg::MrListLoaded {
        generation,
        page: synthetic_mr_list_page(),
    });
    app.update(Msg::GoBack);

    // Render at each screen.
    let screens = [
        Screen::Dashboard,
        Screen::IssueList,
        Screen::MrList,
        Screen::Bootstrap,
    ];

    for screen in &screens {
        app.update(Msg::NavigateTo(screen.clone()));
        let mut frame = Frame::new(80, 24, &mut pool);
        app.view(&mut frame);
    }

    // Render detail screens.
    let issue_key = EntityKey::issue(1, 1);
    navigate_and_load_issue_detail(&mut app, issue_key);
    {
        let mut frame = Frame::new(80, 24, &mut pool);
        app.view(&mut frame);
    }

    app.update(Msg::GoBack);

    let mr_key = EntityKey::mr(1, 10);
    let mr_screen = Screen::MrDetail(mr_key.clone());
    app.update(Msg::NavigateTo(mr_screen.clone()));
    let generation = app
        .supervisor
        .submit(TaskKey::LoadScreen(mr_screen))
        .generation;
    app.update(Msg::MrDetailLoaded {
        generation,
        key: mr_key,
        data: Box::new(synthetic_mr_detail()),
    });
    {
        let mut frame = Frame::new(80, 24, &mut pool);
        app.view(&mut frame);
    }
}

/// Render at various terminal sizes to catch layout panics.
#[test]
fn test_render_various_sizes_no_panic() {
    let mut pool = GraphemePool::new();
    let app = test_app();

    let sizes: [(u16, u16); 5] = [
        (80, 24),  // Standard
        (120, 40), // Large
        (40, 12),  // Small
        (20, 5),   // Very small
        (3, 3),    // Minimum
    ];

    for (w, h) in &sizes {
        let mut frame = Frame::new(*w, *h, &mut pool);
        app.view(&mut frame);
    }
}

// ---------------------------------------------------------------------------
// Navigation depth stress
// ---------------------------------------------------------------------------

/// Navigate deep and verify back-navigation works correctly.
#[test]
fn test_deep_navigation_and_unwind() {
    let mut app = test_app();

    // Navigate through 10 screens.
    for i in 0..5 {
        app.update(Msg::NavigateTo(Screen::IssueList));
        let issue_key = EntityKey::issue(1, i + 1);
        app.update(Msg::NavigateTo(Screen::IssueDetail(issue_key)));
    }

    // Should be at IssueDetail depth.
    assert!(matches!(app.navigation.current(), Screen::IssueDetail(_)));

    // Unwind all the way back to Dashboard.
    for _ in 0..20 {
        app.update(Msg::GoBack);
        if app.navigation.is_at(&Screen::Dashboard) {
            break;
        }
    }

    assert!(
        app.navigation.is_at(&Screen::Dashboard),
        "Should eventually reach Dashboard"
    );
}

// ---------------------------------------------------------------------------
// Performance (smoke test — real benchmarks need criterion)
// ---------------------------------------------------------------------------

/// Verify that 100 update() + view() cycles complete quickly.
/// This is a smoke test, not a precise benchmark.
#[test]
fn test_update_view_cycle_performance_smoke() {
    let mut pool = GraphemePool::new();
    let mut app = test_app();
    load_dashboard(&mut app);

    let start = std::time::Instant::now();
    for _ in 0..100 {
        app.update(Msg::Tick);
        let mut frame = Frame::new(80, 24, &mut pool);
        app.view(&mut frame);
    }
    let elapsed = start.elapsed();

    // 100 cycles should complete in well under 1 second.
    // On a typical machine this takes < 10ms.
    assert!(
        elapsed.as_millis() < 1000,
        "100 update+view cycles took {}ms — too slow",
        elapsed.as_millis()
    );
}
