//! Tests for TUI application.
//!
//! Contains tests for `TuiApp` state machine, input handling, and event dispatch.

#![allow(clippy::unwrap_used, clippy::expect_used)]

use super::*;
use crate::config::NotificationConfig;
use crate::session::SessionStatus;
use crate::tui::popup::PopupSection;
use crate::tui::tabs::{StatusIndicator, TabItem, tab_display_width};
use crate::worktree::GitWorktreeStatus;
use crossterm::event::{
    Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use rstest::rstest;
use std::path::{Path, PathBuf};
use tokio::sync::mpsc;

fn create_test_app() -> (TuiApp, mpsc::Receiver<SessionCommand>) {
    let (cmd_tx, cmd_rx) = mpsc::channel(16);
    // Use default NotificationConfig (bell disabled, webhook disabled)
    let app = TuiApp::new(cmd_tx, NotificationConfig::default(), vec![]);
    (app, cmd_rx)
}

// === Test infrastructure helpers ===

/// Create app with pre-populated sessions
fn create_test_app_with_sessions(count: usize) -> (TuiApp, mpsc::Receiver<SessionCommand>) {
    let (mut app, rx) = create_test_app();
    for i in 0..count {
        let id = SessionId::new_v4();
        app.sessions.push(SessionSnapshot {
            id,
            name: format!("session-{i}"),
            status: SessionStatus::Running,
            branch: Some(format!("feature/{i}")),
        });
        app.terminal_buffers
            .insert(id, vt100::Parser::new(24, 80, 1000));
    }
    if !app.sessions.is_empty() {
        app.popup_state.session_list.select(Some(0));
    }
    (app, rx)
}

/// Create app with pre-populated worktrees
fn create_test_app_with_worktrees(count: usize) -> (TuiApp, mpsc::Receiver<SessionCommand>) {
    let (mut app, rx) = create_test_app();
    for i in 0..count {
        app.worktrees.push(WorktreeItem::new(
            format!("branch-{i}"),
            PathBuf::from(format!("/path/worktree/{i}")),
            GitWorktreeStatus::default(),
        ));
    }
    if !app.worktrees.is_empty() {
        app.popup_state.worktree_list.select(Some(0));
    }
    (app, rx)
}

/// Create terminated session for testing
fn create_terminated_session(id: SessionId, name: &str) -> SessionSnapshot {
    SessionSnapshot {
        id,
        name: name.to_string(),
        status: SessionStatus::Terminated { exit_code: Some(0) },
        branch: None,
    }
}

/// Create `HookEvent` for testing
fn create_test_hook_event(
    event_type: crate::hooks::HookEventType,
    requires_attention: bool,
) -> crate::hooks::HookEvent {
    use serde_json::json;
    let payload = if requires_attention {
        json!({
            "hook_event_name": "Notification",
            "type": "permission_prompt",
            "message": "Test notification"
        })
    } else {
        json!({
            "hook_event_name": event_type.as_hook_name()
        })
    };
    crate::hooks::HookEvent::from_payload(SessionId::new_v4(), payload).unwrap()
}

#[test]
fn test_initial_state_normal() {
    let (app, _rx) = create_test_app();
    assert_eq!(app.state(), &AppState::Normal);
}

// Ctrl key bindings (Normal state) - parametrized
#[rstest]
#[case::ctrl_s('s', Some(AppAction::ShowWorkspacePopup))]
#[case::ctrl_q('q', Some(AppAction::ShowConfirmQuit))]
#[case::ctrl_t('t', Some(AppAction::SendInput(vec![20])))] // ASCII 20
#[case::ctrl_w('w', Some(AppAction::TerminateCurrentSession))]
#[case::ctrl_n('n', Some(AppAction::NextSession))]
#[case::ctrl_p('p', Some(AppAction::PrevSession))]
fn ctrl_key_normal_state(#[case] ch: char, #[case] expected: Option<AppAction>) {
    let (mut app, _rx) = create_test_app();
    let key = KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL);
    assert_eq!(app.handle_key(key), expected);
}

#[test]
fn test_esc_hides_popup() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
    assert_eq!(app.handle_key(key), Some(AppAction::HidePopup));
}

#[test]
fn test_session_created_event() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();

    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });

    assert_eq!(app.sessions.len(), 1);
    assert_eq!(app.sessions[0].id, id);
    assert!(app.terminal_buffers.contains_key(&id));
}

#[test]
fn test_session_output_event() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();

    // Create session first
    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });

    // Send output
    app.handle_session_event(SessionEvent::Output {
        id,
        data: b"Hello".to_vec(),
    });

    // Check buffer was updated
    let parser = app.terminal_buffers.get(&id).unwrap();
    let screen = parser.screen();
    let mut content = String::new();
    for col in 0..5 {
        if let Some(cell) = screen.cell(0, col) {
            content.push_str(&cell.contents());
        }
    }
    assert_eq!(content, "Hello");
}

#[test]
fn test_session_title_changed_event() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();

    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });
    app.handle_session_event(SessionEvent::TitleChanged {
        id,
        title: "My Session".to_string(),
    });

    assert_eq!(app.sessions[0].name, "My Session");
}

#[test]
fn test_session_terminated_event() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();

    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });
    app.handle_session_event(SessionEvent::Terminated {
        id,
        exit_code: Some(0),
    });

    assert!(matches!(
        app.sessions[0].status,
        SessionStatus::Terminated { exit_code: Some(0) }
    ));
}

#[test]
fn test_next_session_wraps() {
    let (mut app, _rx) = create_test_app();

    // Create 3 sessions
    for _ in 0..3 {
        app.handle_session_event(SessionEvent::Created {
            id: SessionId::new_v4(),
            branch: None,
            auto_input: Vec::new(),
        });
    }

    app.active_idx = 2;
    app.next_session();
    assert_eq!(app.active_idx, 0);
}

#[test]
fn test_prev_session_wraps() {
    let (mut app, _rx) = create_test_app();

    // Create 3 sessions
    for _ in 0..3 {
        app.handle_session_event(SessionEvent::Created {
            id: SessionId::new_v4(),
            branch: None,
            auto_input: Vec::new(),
        });
    }

    app.active_idx = 0;
    app.prev_session();
    assert_eq!(app.active_idx, 2);
}

#[test]
fn test_switch_session() {
    let (mut app, _rx) = create_test_app();

    for _ in 0..3 {
        app.handle_session_event(SessionEvent::Created {
            id: SessionId::new_v4(),
            branch: None,
            auto_input: Vec::new(),
        });
    }

    app.switch_session(1);
    assert_eq!(app.active_idx, 1);

    // Out of bounds should not change
    app.switch_session(10);
    assert_eq!(app.active_idx, 1);
}

#[test]
fn test_scroll() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });

    app.scroll(-5);
    assert_eq!(app.scroll_offsets.get(&id), Some(&5));

    app.scroll(3);
    assert_eq!(app.scroll_offsets.get(&id), Some(&2));

    app.scroll(10);
    assert_eq!(app.scroll_offsets.get(&id), Some(&0));
}

#[test]
fn test_confirm_quit_yes() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmQuit;

    let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
    let action = app.handle_key(key);

    assert_eq!(action, Some(AppAction::ConfirmQuit));
}

#[test]
fn test_confirm_quit_no() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmQuit;

    let key = KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE);
    let action = app.handle_key(key);

    assert_eq!(action, Some(AppAction::HidePopup));
}

#[test]
fn test_workspace_popup_session_navigation() {
    let (mut app, _rx) = create_test_app();

    for _ in 0..3 {
        app.handle_session_event(SessionEvent::Created {
            id: SessionId::new_v4(),
            branch: None,
            auto_input: Vec::new(),
        });
    }

    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(0));

    // Select next
    app.select_next();
    assert_eq!(app.popup_state.session_list.selected(), Some(1));

    // Select prev
    app.select_prev();
    assert_eq!(app.popup_state.session_list.selected(), Some(0));

    // Wrap around
    app.select_prev();
    assert_eq!(app.popup_state.session_list.selected(), Some(2));
}

#[tokio::test]
async fn test_dispatch_hide_popup() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    app.dispatch(AppAction::HidePopup).await.unwrap();
    assert_eq!(app.state, AppState::Normal);
}

#[tokio::test]
async fn test_dispatch_show_workspace_popup() {
    let (mut app, _rx) = create_test_app();

    app.dispatch(AppAction::ShowWorkspacePopup).await.unwrap();
    assert_eq!(app.state, AppState::WorkspacePopup);
}

#[tokio::test]
async fn test_dispatch_confirm_quit() {
    let (mut app, _rx) = create_test_app();

    app.dispatch(AppAction::ConfirmQuit).await.unwrap();
    assert!(app.should_quit());
}

// New workspace popup action tests
#[tokio::test]
async fn test_dispatch_input_char() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    app.dispatch(AppAction::InputChar('f')).await.unwrap();
    app.dispatch(AppAction::InputChar('o')).await.unwrap();
    app.dispatch(AppAction::InputChar('o')).await.unwrap();

    assert_eq!(app.popup_state.input, "foo");
    assert_eq!(app.popup_state.cursor, 3);
}

#[tokio::test]
async fn test_dispatch_input_backspace() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    app.dispatch(AppAction::InputChar('a')).await.unwrap();
    app.dispatch(AppAction::InputChar('b')).await.unwrap();
    app.dispatch(AppAction::InputBackspace).await.unwrap();

    assert_eq!(app.popup_state.input, "a");
}

#[tokio::test]
async fn test_dispatch_next_prev_popup_section() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    assert_eq!(app.popup_state.section, PopupSection::BranchInput);

    app.dispatch(AppAction::NextPopupSection).await.unwrap();
    assert_eq!(app.popup_state.section, PopupSection::Sessions);

    app.dispatch(AppAction::NextPopupSection).await.unwrap();
    assert_eq!(app.popup_state.section, PopupSection::Worktrees);

    app.dispatch(AppAction::PrevPopupSection).await.unwrap();
    assert_eq!(app.popup_state.section, PopupSection::Sessions);
}

#[test]
fn test_workspace_popup_tab_switches_section() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    let key = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
    let action = app.handle_key(key);

    assert_eq!(action, Some(AppAction::NextPopupSection));
}

#[test]
fn test_workspace_popup_branch_input_enter() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::BranchInput;
    app.popup_state.input = "feature/test".to_string();

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
    let action = app.handle_key(key);

    assert_eq!(
        action,
        Some(AppAction::CreateSessionWithBranch(
            "feature/test".to_string()
        ))
    );
}

#[test]
fn test_workspace_popup_branch_input_enter_empty() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::BranchInput;
    // input is empty by default

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
    let action = app.handle_key(key);

    // Empty input should not create session
    assert_eq!(action, None);
}

// === Phase 2: Additional coverage tests ===

#[test]
fn test_session_count() {
    let (mut app, _rx) = create_test_app();
    assert_eq!(app.session_count(), 0);

    app.handle_session_event(SessionEvent::Created {
        id: SessionId::new_v4(),
        branch: None,
        auto_input: Vec::new(),
    });
    assert_eq!(app.session_count(), 1);
}

#[test]
fn test_set_size() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });

    app.set_size(50, 120);
    assert_eq!(app.size, (50, 120));
}

#[test]
fn test_handle_event_key() {
    let (mut app, _rx) = create_test_app();
    let event = Event::Key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL));
    let action = app.handle_event(&event);
    assert_eq!(action, Some(AppAction::ShowConfirmQuit));
}

#[test]
fn test_handle_event_mouse_scroll() {
    let (mut app, _rx) = create_test_app();
    let event = Event::Mouse(MouseEvent {
        kind: MouseEventKind::ScrollUp,
        column: 0,
        row: 0,
        modifiers: KeyModifiers::NONE,
    });
    let action = app.handle_event(&event);
    assert_eq!(action, Some(AppAction::Scroll(-3)));
}

#[test]
fn test_handle_event_resize() {
    let (mut app, _rx) = create_test_app();
    let event = Event::Resize(100, 40);
    let action = app.handle_event(&event);
    assert_eq!(action, Some(AppAction::ResizeTerminal(40, 100)));
}

// Key forwarding tests (normal mode) - parametrized
#[rstest]
#[case::enter(KeyCode::Enter, Some(AppAction::SendInput(vec![b'\r'])))]
#[case::backspace(KeyCode::Backspace, Some(AppAction::SendInput(vec![0x7f])))]
#[case::tab(KeyCode::Tab, Some(AppAction::SendInput(vec![b'\t'])))]
#[case::backtab(KeyCode::BackTab, Some(AppAction::SendInput(b"\x1b[Z".to_vec())))]
#[case::escape(KeyCode::Esc, Some(AppAction::SendInput(vec![0x1b])))]
#[case::up(KeyCode::Up, Some(AppAction::SendInput(b"\x1b[A".to_vec())))]
#[case::down(KeyCode::Down, Some(AppAction::SendInput(b"\x1b[B".to_vec())))]
#[case::right(KeyCode::Right, Some(AppAction::SendInput(b"\x1b[C".to_vec())))]
#[case::left(KeyCode::Left, Some(AppAction::SendInput(b"\x1b[D".to_vec())))]
#[case::home(KeyCode::Home, Some(AppAction::SendInput(b"\x1b[H".to_vec())))]
#[case::end(KeyCode::End, Some(AppAction::SendInput(b"\x1b[F".to_vec())))]
#[case::delete(KeyCode::Delete, Some(AppAction::SendInput(b"\x1b[3~".to_vec())))]
#[case::page_up(KeyCode::PageUp, Some(AppAction::Scroll(-10)))]
#[case::page_down(KeyCode::PageDown, Some(AppAction::Scroll(10)))]
fn key_forward(#[case] code: KeyCode, #[case] expected: Option<AppAction>) {
    let (mut app, _rx) = create_test_app();
    assert_eq!(
        app.handle_key(KeyEvent::new(code, KeyModifiers::NONE)),
        expected
    );
}

// Mouse handler tests
#[test]
fn test_mouse_click_tab_bar() {
    let (mut app, _rx) = create_test_app();
    // Create 3 sessions ("session-1", "session-2", "session-3")
    for _ in 0..3 {
        app.handle_session_event(SessionEvent::Created {
            id: SessionId::new_v4(),
            branch: None,
            auto_input: Vec::new(),
        });
    }

    // Each session-N tab has width = indicator(2) + title(9) + sep(3) = 14
    let tab1_width = tab_display_width(&TabItem::new("session-1", StatusIndicator::Running));
    assert_eq!(tab1_width, 14);

    // Click on first tab (row 0, column 0)
    let mouse = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: 0,
        row: 0,
        modifiers: KeyModifiers::NONE,
    };
    let action = app.handle_mouse(mouse);
    assert_eq!(action, Some(AppAction::SwitchSession(0)));

    // Click on second tab (row 0, column 14 = start of tab 2)
    let mouse = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: tab1_width,
        row: 0,
        modifiers: KeyModifiers::NONE,
    };
    let action = app.handle_mouse(mouse);
    assert_eq!(action, Some(AppAction::SwitchSession(1)));
}

#[test]
fn test_mouse_click_tab_bar_out_of_bounds() {
    let (mut app, _rx) = create_test_app();
    app.handle_session_event(SessionEvent::Created {
        id: SessionId::new_v4(),
        branch: None,
        auto_input: Vec::new(),
    });

    // "session-1" width = 14; click beyond should return None
    let tab_width = tab_display_width(&TabItem::new("session-1", StatusIndicator::Running));

    // Click beyond existing tabs
    let mouse = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: tab_width * 5,
        row: 0,
        modifiers: KeyModifiers::NONE,
    };
    let action = app.handle_mouse(mouse);
    assert_eq!(action, None);
}

#[test]
fn test_mouse_click_tab_bar_variable_width() {
    let (mut app, _rx) = create_test_app();

    // Create sessions with different name widths
    let id1 = SessionId::new_v4();
    let id2 = SessionId::new_v4();
    let id3 = SessionId::new_v4();

    // Create sessions and rename them
    app.handle_session_event(SessionEvent::Created {
        id: id1,
        branch: None,
        auto_input: Vec::new(),
    });
    app.handle_session_event(SessionEvent::TitleChanged {
        id: id1,
        title: "a".to_string(), // width = 2+1+3 = 6
    });

    app.handle_session_event(SessionEvent::Created {
        id: id2,
        branch: None,
        auto_input: Vec::new(),
    });
    app.handle_session_event(SessionEvent::TitleChanged {
        id: id2,
        title: "日本語".to_string(), // width = 2+6+3 = 11 (CJK: 3 chars × 2 cells)
    });

    app.handle_session_event(SessionEvent::Created {
        id: id3,
        branch: None,
        auto_input: Vec::new(),
    });
    app.handle_session_event(SessionEvent::TitleChanged {
        id: id3,
        title: "test".to_string(), // width = 2+4+3 = 9
    });

    // Verify widths
    let tab1_width = tab_display_width(&TabItem::new("a", StatusIndicator::Running));
    let tab2_width = tab_display_width(&TabItem::new("日本語", StatusIndicator::Running));
    let tab3_width = tab_display_width(&TabItem::new("test", StatusIndicator::Running));
    assert_eq!(tab1_width, 6);
    assert_eq!(tab2_width, 11);
    assert_eq!(tab3_width, 9);

    // Tab boundaries: tab1=[0,5], tab2=[6,16], tab3=[17,25]

    // Click last column of tab1
    let mouse = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: 5,
        row: 0,
        modifiers: KeyModifiers::NONE,
    };
    assert_eq!(app.handle_mouse(mouse), Some(AppAction::SwitchSession(0)));

    // Click first column of tab2
    let mouse = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: 6,
        row: 0,
        modifiers: KeyModifiers::NONE,
    };
    assert_eq!(app.handle_mouse(mouse), Some(AppAction::SwitchSession(1)));

    // Click last column of tab2
    let mouse = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: 16,
        row: 0,
        modifiers: KeyModifiers::NONE,
    };
    assert_eq!(app.handle_mouse(mouse), Some(AppAction::SwitchSession(1)));

    // Click first column of tab3
    let mouse = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: 17,
        row: 0,
        modifiers: KeyModifiers::NONE,
    };
    assert_eq!(app.handle_mouse(mouse), Some(AppAction::SwitchSession(2)));

    // Click beyond all tabs
    let mouse = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: 26,
        row: 0,
        modifiers: KeyModifiers::NONE,
    };
    assert_eq!(app.handle_mouse(mouse), None);
}

#[test]
fn test_mouse_click_not_tab_bar() {
    let (mut app, _rx) = create_test_app();
    app.handle_session_event(SessionEvent::Created {
        id: SessionId::new_v4(),
        branch: None,
        auto_input: Vec::new(),
    });

    // Click below tab bar
    let mouse = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: 0,
        row: 5,
        modifiers: KeyModifiers::NONE,
    };
    let action = app.handle_mouse(mouse);
    assert_eq!(action, None);
}

#[test]
fn test_mouse_scroll_up_down() {
    let (mut app, _rx) = create_test_app();

    let action = app.handle_mouse(MouseEvent {
        kind: MouseEventKind::ScrollUp,
        column: 0,
        row: 0,
        modifiers: KeyModifiers::NONE,
    });
    assert_eq!(action, Some(AppAction::Scroll(-3)));

    let action = app.handle_mouse(MouseEvent {
        kind: MouseEventKind::ScrollDown,
        column: 0,
        row: 0,
        modifiers: KeyModifiers::NONE,
    });
    assert_eq!(action, Some(AppAction::Scroll(3)));
}

#[test]
fn test_mouse_drag_returns_none() {
    let (mut app, _rx) = create_test_app();

    let action = app.handle_mouse(MouseEvent {
        kind: MouseEventKind::Drag(MouseButton::Left),
        column: 0,
        row: 0,
        modifiers: KeyModifiers::NONE,
    });
    assert_eq!(action, None);
}

#[test]
fn test_mouse_moved_returns_none() {
    let (mut app, _rx) = create_test_app();

    let action = app.handle_mouse(MouseEvent {
        kind: MouseEventKind::Moved,
        column: 10,
        row: 10,
        modifiers: KeyModifiers::NONE,
    });
    assert_eq!(action, None);
}

// dispatch() tests
#[tokio::test]
async fn test_dispatch_switch_session() {
    let (mut app, _rx) = create_test_app();
    for _ in 0..3 {
        app.handle_session_event(SessionEvent::Created {
            id: SessionId::new_v4(),
            branch: None,
            auto_input: Vec::new(),
        });
    }

    app.dispatch(AppAction::SwitchSession(1)).await.unwrap();
    assert_eq!(app.active_idx, 1);
}

#[tokio::test]
async fn test_dispatch_next_prev_session() {
    let (mut app, _rx) = create_test_app();
    for _ in 0..3 {
        app.handle_session_event(SessionEvent::Created {
            id: SessionId::new_v4(),
            branch: None,
            auto_input: Vec::new(),
        });
    }
    app.active_idx = 0;

    app.dispatch(AppAction::NextSession).await.unwrap();
    assert_eq!(app.active_idx, 1);

    app.dispatch(AppAction::PrevSession).await.unwrap();
    assert_eq!(app.active_idx, 0);
}

#[tokio::test]
async fn test_dispatch_scroll() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });

    app.dispatch(AppAction::Scroll(-5)).await.unwrap();
    assert_eq!(app.scroll_offsets.get(&id), Some(&5));
}

#[tokio::test]
async fn test_dispatch_resize_terminal() {
    let (mut app, mut rx) = create_test_app();
    let id = SessionId::new_v4();
    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });

    app.dispatch(AppAction::ResizeTerminal(50, 120))
        .await
        .unwrap();
    assert_eq!(app.size, (50, 120));

    // Should send resize command to session
    if let Ok(SessionCommand::Resize { rows, cols, .. }) = rx.try_recv() {
        assert_eq!(rows, 50);
        assert_eq!(cols, 120);
    }
}

#[tokio::test]
async fn test_dispatch_toggle_quit_selection() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmQuit;
    app.quit_selected_yes = false;

    app.dispatch(AppAction::ToggleQuitSelection).await.unwrap();
    assert!(app.quit_selected_yes);

    app.dispatch(AppAction::ToggleQuitSelection).await.unwrap();
    assert!(!app.quit_selected_yes);
}

#[tokio::test]
async fn test_dispatch_select_next_prev() {
    let (mut app, _rx) = create_test_app();
    for _ in 0..3 {
        app.handle_session_event(SessionEvent::Created {
            id: SessionId::new_v4(),
            branch: None,
            auto_input: Vec::new(),
        });
    }
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(0));

    app.dispatch(AppAction::SelectNext).await.unwrap();
    assert_eq!(app.popup_state.session_list.selected(), Some(1));

    app.dispatch(AppAction::SelectPrev).await.unwrap();
    assert_eq!(app.popup_state.session_list.selected(), Some(0));
}

#[tokio::test]
async fn test_dispatch_confirm_selection() {
    let (mut app, _rx) = create_test_app();
    for _ in 0..3 {
        app.handle_session_event(SessionEvent::Created {
            id: SessionId::new_v4(),
            branch: None,
            auto_input: Vec::new(),
        });
    }
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(2));

    app.dispatch(AppAction::ConfirmSelection).await.unwrap();
    assert_eq!(app.active_idx, 2);
    assert_eq!(app.state, AppState::Normal);
}

#[tokio::test]
async fn test_dispatch_send_input() {
    let (mut app, mut rx) = create_test_app();
    let id = SessionId::new_v4();
    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });

    app.dispatch(AppAction::SendInput(b"hello".to_vec()))
        .await
        .unwrap();

    if let Ok(SessionCommand::SendInput { data, .. }) = rx.try_recv() {
        assert_eq!(data, b"hello");
    }
}

#[tokio::test]
async fn test_dispatch_input_cursor_left_right() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    app.dispatch(AppAction::InputChar('a')).await.unwrap();
    app.dispatch(AppAction::InputChar('b')).await.unwrap();
    assert_eq!(app.popup_state.cursor, 2);

    app.dispatch(AppAction::InputCursorLeft).await.unwrap();
    assert_eq!(app.popup_state.cursor, 1);

    app.dispatch(AppAction::InputCursorRight).await.unwrap();
    assert_eq!(app.popup_state.cursor, 2);
}

// Workspace popup section-specific key tests
#[test]
fn test_workspace_popup_backtab() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    let key = KeyEvent::new(KeyCode::BackTab, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::PrevPopupSection));
}

#[test]
fn test_workspace_popup_ctrl_q_shows_confirm() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::ShowConfirmQuit));
}

#[test]
fn test_workspace_popup_branch_input_keys() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::BranchInput;

    let action = app.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::InputChar('x')));

    let action = app.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::InputBackspace));

    let action = app.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::InputCursorLeft));

    let action = app.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::InputCursorRight));
}

#[test]
fn test_workspace_popup_session_section_keys() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Sessions;

    let action = app.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::SelectNext));

    let action = app.handle_key(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::SelectPrev));

    let action = app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::ConfirmSelection));
}

#[test]
fn test_workspace_popup_worktree_section_keys() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Worktrees;

    // Add worktree item for testing
    app.worktrees.push(WorktreeItem::new(
        "test-branch",
        PathBuf::from("/path/to/worktree"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.worktree_list.select(Some(0));

    let action = app.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::SelectNext));

    let action = app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
    assert_eq!(
        action,
        Some(AppAction::AdoptWorktree {
            path: PathBuf::from("/path/to/worktree")
        })
    );

    let action = app.handle_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
    assert_eq!(
        action,
        Some(AppAction::DeleteWorktree {
            path: PathBuf::from("/path/to/worktree")
        })
    );

    let action = app.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE));
    assert_eq!(
        action,
        Some(AppAction::PullWorktree {
            path: PathBuf::from("/path/to/worktree")
        })
    );
}

#[test]
fn test_workspace_popup_worktree_no_selection() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Worktrees;
    // No worktrees, no selection

    let action = app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
    assert_eq!(action, None);

    let action = app.handle_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
    assert_eq!(action, None);
}

#[test]
fn test_confirm_quit_toggle() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmQuit;

    let action = app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::ToggleQuitSelection));

    let action = app.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::ToggleQuitSelection));

    let action = app.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE));
    assert_eq!(action, Some(AppAction::ToggleQuitSelection));
}

#[test]
fn test_session_error_event() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();

    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });
    // Error event should be logged (no state change)
    app.handle_session_event(SessionEvent::Error {
        id,
        error: "Test error".to_string(),
    });

    // Session should still exist
    assert_eq!(app.sessions.len(), 1);
}

// Worktree section navigation tests
#[test]
fn test_select_next_worktrees_section() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Worktrees;

    // Add worktrees
    for i in 0..3 {
        app.worktrees.push(WorktreeItem::new(
            format!("branch-{i}"),
            PathBuf::from(format!("/path/{i}")),
            GitWorktreeStatus::default(),
        ));
    }
    app.popup_state.worktree_list.select(Some(0));

    app.select_next();
    assert_eq!(app.popup_state.worktree_list.selected(), Some(1));

    app.select_next();
    assert_eq!(app.popup_state.worktree_list.selected(), Some(2));

    // Wrap around
    app.select_next();
    assert_eq!(app.popup_state.worktree_list.selected(), Some(0));
}

#[test]
fn test_select_prev_worktrees_section() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Worktrees;

    for i in 0..3 {
        app.worktrees.push(WorktreeItem::new(
            format!("branch-{i}"),
            PathBuf::from(format!("/path/{i}")),
            GitWorktreeStatus::default(),
        ));
    }
    app.popup_state.worktree_list.select(Some(0));

    // Wrap around backward
    app.select_prev();
    assert_eq!(app.popup_state.worktree_list.selected(), Some(2));

    app.select_prev();
    assert_eq!(app.popup_state.worktree_list.selected(), Some(1));
}

#[test]
fn test_select_next_empty_worktrees() {
    let (mut app, _rx) = create_test_app();
    app.popup_state.section = PopupSection::Worktrees;
    // No worktrees
    app.select_next();
    // Should not panic, selection stays None
    assert_eq!(app.popup_state.worktree_list.selected(), None);
}

#[test]
fn test_select_next_branch_input_noop() {
    let (mut app, _rx) = create_test_app();
    app.popup_state.section = PopupSection::BranchInput;
    // select_next should do nothing in BranchInput section
    app.select_next();
    app.select_prev();
    // No assertion needed, just verify no panic
}

// Navigation on empty sessions does nothing
#[test]
fn session_nav_empty_noop() {
    let (mut app, _rx) = create_test_app();
    app.next_session();
    assert_eq!(app.active_idx, 0);
    app.prev_session();
    assert_eq!(app.active_idx, 0);
}

#[test]
fn test_scroll_no_session() {
    let (mut app, _rx) = create_test_app();
    // No sessions, scroll should do nothing
    app.scroll(-5);
    assert!(app.scroll_offsets.is_empty());
}

// === Additional edge case tests ===

// handle_event non-key events return None
#[test]
fn handle_event_non_key_returns_none() {
    let (mut app, _rx) = create_test_app();
    assert_eq!(app.handle_event(&Event::FocusGained), None);
    assert_eq!(app.handle_event(&Event::FocusLost), None);
    assert_eq!(app.handle_event(&Event::Paste("test".to_string())), None);
}

// Unhandled keys return None in Normal state
#[test]
fn unhandled_keys_return_none() {
    let (mut app, _rx) = create_test_app();
    // Ctrl+F1, F12 - unhandled in Normal state
    assert_eq!(
        app.handle_key(KeyEvent::new(KeyCode::F(1), KeyModifiers::CONTROL)),
        None
    );
    assert_eq!(
        app.handle_key(KeyEvent::new(KeyCode::F(12), KeyModifiers::NONE)),
        None
    );
}

#[test]
fn test_key_forward_regular_char() {
    let (mut app, _rx) = create_test_app();
    let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::SendInput(b"x".to_vec())));
}

// Unhandled keys in various popup states return None
#[test]
fn unhandled_keys_in_popup_states() {
    let (mut app, _rx) = create_test_app();

    // WorkspacePopup BranchInput - F5, Up, Down return None
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::BranchInput;
    assert_eq!(
        app.handle_key(KeyEvent::new(KeyCode::F(5), KeyModifiers::NONE)),
        None
    );
    assert_eq!(
        app.handle_key(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)),
        None
    );
    assert_eq!(
        app.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)),
        None
    );

    // ConfirmQuit - F5 returns None
    app.state = AppState::ConfirmQuit;
    assert_eq!(
        app.handle_key(KeyEvent::new(KeyCode::F(5), KeyModifiers::NONE)),
        None
    );

    // ErrorPopup - 'x' returns None
    app.state = AppState::ErrorPopup {
        message: "test".to_string(),
        from_popup: false,
    };
    assert_eq!(
        app.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)),
        None
    );
}

// === Error popup tests ===

#[test]
fn test_error_popup_esc_dismisses() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ErrorPopup {
        message: "test error".to_string(),
        from_popup: false,
    };

    let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::DismissError));
}

#[test]
fn test_error_popup_enter_dismisses() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ErrorPopup {
        message: "test error".to_string(),
        from_popup: false,
    };

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::DismissError));
}

#[tokio::test]
async fn test_dispatch_dismiss_error_to_normal() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ErrorPopup {
        message: "test error".to_string(),
        from_popup: false,
    };

    app.dispatch(AppAction::DismissError).await.unwrap();
    assert_eq!(app.state, AppState::Normal);
}

#[tokio::test]
async fn test_dispatch_dismiss_error_to_workspace_popup() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ErrorPopup {
        message: "test error".to_string(),
        from_popup: true,
    };

    app.dispatch(AppAction::DismissError).await.unwrap();
    assert_eq!(app.state, AppState::WorkspacePopup);
}

#[test]
fn test_show_error_from_normal() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::Normal;

    app.show_error("test error".to_string());

    assert!(matches!(
        app.state,
        AppState::ErrorPopup {
            from_popup: false,
            ..
        }
    ));
}

#[test]
fn test_show_error_from_workspace_popup() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    app.show_error("test error".to_string());

    assert!(matches!(
        app.state,
        AppState::ErrorPopup {
            from_popup: true,
            ..
        }
    ));
}

#[test]
fn test_worktree_pulled_updates_status() {
    let (mut app, _rx) = create_test_app();
    let path = PathBuf::from("/test/worktree");

    // Add a worktree item
    app.worktrees.push(WorktreeItem {
        display_name: "worktree".to_string(),
        branch: "main".to_string(),
        path: path.clone(),
        status: GitWorktreeStatus::default(),
    });

    // Simulate WorktreePulled event with new status
    let new_status = GitWorktreeStatus {
        dirty: true,
        ahead: 2,
        behind: 1,
        detached: false,
        no_upstream: false,
    };
    app.handle_session_event(SessionEvent::WorktreePulled {
        path: path.clone(),
        result: Ok(new_status.clone()),
    });

    // Status should be updated
    assert_eq!(app.worktrees[0].status, new_status);
    assert_eq!(app.popup_state.loading, LoadingOperation::Idle);
}

#[test]
fn test_worktree_pulled_error_shows_error() {
    let (mut app, _rx) = create_test_app();
    let path = PathBuf::from("/test/worktree");

    app.handle_session_event(SessionEvent::WorktreePulled {
        path,
        result: Err("Pull failed".to_string()),
    });

    assert!(matches!(app.state, AppState::ErrorPopup { .. }));
    assert_eq!(app.popup_state.loading, LoadingOperation::Idle);
}

// === Emacs-style navigation tests ===

#[test]
fn test_workspace_popup_ctrl_g_hides_popup() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    let key = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::HidePopup));
}

#[test]
fn test_workspace_popup_ctrl_n_cross_section_next() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    let key = KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CrossSectionNext));
}

#[test]
fn test_workspace_popup_ctrl_p_cross_section_prev() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    let key = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CrossSectionPrev));
}

#[test]
fn test_workspace_popup_ctrl_m_confirm_in_sessions() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Sessions;

    let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::ConfirmSelection));
}

#[test]
fn test_workspace_popup_ctrl_m_confirm_in_branch_input_empty() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::BranchInput;
    app.popup_state.input.clear();

    let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

#[test]
fn test_workspace_popup_ctrl_m_confirm_in_branch_input_with_text() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::BranchInput;
    app.popup_state.input = "feature-branch".to_string();

    let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(
        action,
        Some(AppAction::CreateSessionWithBranch(
            "feature-branch".to_string()
        ))
    );
}

#[test]
fn test_workspace_popup_ctrl_m_confirm_in_worktrees() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Worktrees;
    app.worktrees.push(WorktreeItem::new(
        "test-branch",
        PathBuf::from("/path/to/worktree"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.worktree_list.select(Some(0));

    let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(
        action,
        Some(AppAction::AdoptWorktree {
            path: PathBuf::from("/path/to/worktree")
        })
    );
}

#[test]
fn test_cross_section_next_from_branch_input_to_sessions() {
    let (mut app, _rx) = create_test_app();
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-1".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.popup_state.section = PopupSection::BranchInput;

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::Sessions);
    assert_eq!(app.popup_state.session_list.selected(), Some(0));
}

#[test]
fn test_cross_section_next_from_branch_input_to_worktrees_no_sessions() {
    let (mut app, _rx) = create_test_app();
    app.sessions.clear();
    app.worktrees.push(WorktreeItem::new(
        "branch",
        PathBuf::from("/path"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.section = PopupSection::BranchInput;

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::Worktrees);
    assert_eq!(app.popup_state.worktree_list.selected(), Some(0));
}

#[test]
fn test_cross_section_next_within_sessions() {
    let (mut app, _rx) = create_test_app();
    // Add two sessions
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-1".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-2".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(0));

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::Sessions);
    assert_eq!(app.popup_state.session_list.selected(), Some(1));
}

#[test]
fn test_cross_section_next_sessions_to_worktrees() {
    let (mut app, _rx) = create_test_app();
    app.worktrees.push(WorktreeItem::new(
        "branch",
        PathBuf::from("/path"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(0)); // Last session

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::Worktrees);
    assert_eq!(app.popup_state.worktree_list.selected(), Some(0));
}

#[test]
fn test_cross_section_next_sessions_to_branch_input_no_worktrees() {
    let (mut app, _rx) = create_test_app();
    app.worktrees.clear();
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(0));

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::BranchInput);
}

#[test]
fn test_cross_section_next_worktrees_wrap_to_branch_input() {
    let (mut app, _rx) = create_test_app();
    app.worktrees.push(WorktreeItem::new(
        "branch",
        PathBuf::from("/path"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.section = PopupSection::Worktrees;
    app.popup_state.worktree_list.select(Some(0));

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::BranchInput);
}

#[test]
fn test_cross_section_prev_from_branch_input_to_worktrees() {
    let (mut app, _rx) = create_test_app();
    app.worktrees.push(WorktreeItem::new(
        "branch",
        PathBuf::from("/path"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.section = PopupSection::BranchInput;

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::Worktrees);
    assert_eq!(app.popup_state.worktree_list.selected(), Some(0));
}

#[test]
fn test_cross_section_prev_from_branch_input_to_sessions_no_worktrees() {
    let (mut app, _rx) = create_test_app();
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-1".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.worktrees.clear();
    app.popup_state.section = PopupSection::BranchInput;

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::Sessions);
    assert_eq!(app.popup_state.session_list.selected(), Some(0));
}

#[test]
fn test_cross_section_prev_within_sessions() {
    let (mut app, _rx) = create_test_app();
    // Add two sessions
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-1".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-2".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(1));

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::Sessions);
    assert_eq!(app.popup_state.session_list.selected(), Some(0));
}

#[test]
fn test_cross_section_prev_sessions_to_branch_input() {
    let (mut app, _rx) = create_test_app();
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(0));

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::BranchInput);
}

#[test]
fn test_cross_section_prev_worktrees_to_sessions() {
    let (mut app, _rx) = create_test_app();
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-1".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.worktrees.push(WorktreeItem::new(
        "branch",
        PathBuf::from("/path"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.section = PopupSection::Worktrees;
    app.popup_state.worktree_list.select(Some(0));

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::Sessions);
    assert_eq!(app.popup_state.session_list.selected(), Some(0));
}

#[test]
fn test_cross_section_prev_worktrees_to_branch_input_no_sessions() {
    let (mut app, _rx) = create_test_app();
    app.sessions.clear();
    app.worktrees.push(WorktreeItem::new(
        "branch",
        PathBuf::from("/path"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.section = PopupSection::Worktrees;
    app.popup_state.worktree_list.select(Some(0));

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::BranchInput);
}

#[tokio::test]
async fn test_dispatch_cross_section_next() {
    let (mut app, _rx) = create_test_app();
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-1".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.popup_state.section = PopupSection::BranchInput;

    app.dispatch(AppAction::CrossSectionNext).await.unwrap();

    assert_eq!(app.popup_state.section, PopupSection::Sessions);
}

#[tokio::test]
async fn test_dispatch_cross_section_prev() {
    let (mut app, _rx) = create_test_app();
    app.worktrees.push(WorktreeItem::new(
        "branch",
        PathBuf::from("/path"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.section = PopupSection::BranchInput;

    app.dispatch(AppAction::CrossSectionPrev).await.unwrap();

    assert_eq!(app.popup_state.section, PopupSection::Worktrees);
}

// Session close tests
#[test]
fn test_session_terminated_popup_shows_on_active_session() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();

    app.handle_session_event(SessionEvent::Created {
        id,
        branch: None,
        auto_input: Vec::new(),
    });

    // Terminate the active session
    app.handle_session_event(SessionEvent::Terminated {
        id,
        exit_code: Some(0),
    });

    // Should show termination popup
    assert!(matches!(
        app.state,
        AppState::SessionTerminatedPopup {
            session_id,
            exit_code: Some(0)
        } if session_id == id
    ));
}

#[test]
fn test_session_terminated_popup_not_shown_for_inactive_session() {
    let (mut app, _rx) = create_test_app();
    let id1 = SessionId::new_v4();
    let id2 = SessionId::new_v4();

    app.handle_session_event(SessionEvent::Created {
        id: id1,
        branch: None,
        auto_input: Vec::new(),
    });
    app.handle_session_event(SessionEvent::Created {
        id: id2,
        branch: None,
        auto_input: Vec::new(),
    });

    // Switch to first session (id1)
    app.active_idx = 0;

    // Terminate id2 (not active)
    app.handle_session_event(SessionEvent::Terminated {
        id: id2,
        exit_code: Some(0),
    });

    // Should NOT show popup for non-active session
    assert_eq!(app.state, AppState::Normal);
}

#[test]
fn test_session_terminated_popup_key_close_lowercase() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.sessions.push(SessionSnapshot {
        id,
        name: "test".to_string(),
        status: SessionStatus::Terminated { exit_code: Some(0) },
        branch: None,
    });
    app.state = AppState::SessionTerminatedPopup {
        session_id: id,
        exit_code: Some(0),
    };

    let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE);
    let action = app.handle_key(key);

    assert_eq!(action, Some(AppAction::CloseSession { id }));
    assert_eq!(app.state, AppState::Normal);
}

#[test]
fn test_session_terminated_popup_key_close_uppercase() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.sessions.push(SessionSnapshot {
        id,
        name: "test".to_string(),
        status: SessionStatus::Terminated { exit_code: Some(0) },
        branch: None,
    });
    app.state = AppState::SessionTerminatedPopup {
        session_id: id,
        exit_code: Some(0),
    };

    let key = KeyEvent::new(KeyCode::Char('C'), KeyModifiers::NONE);
    let action = app.handle_key(key);

    assert_eq!(action, Some(AppAction::CloseSession { id }));
    assert_eq!(app.state, AppState::Normal);
}

#[test]
fn test_session_terminated_popup_key_keep_uppercase() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.state = AppState::SessionTerminatedPopup {
        session_id: id,
        exit_code: Some(0),
    };

    let key = KeyEvent::new(KeyCode::Char('N'), KeyModifiers::NONE);
    let action = app.handle_key(key);

    assert_eq!(action, None);
    assert_eq!(app.state, AppState::Normal);
}

#[test]
fn test_session_terminated_popup_unhandled_key() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.state = AppState::SessionTerminatedPopup {
        session_id: id,
        exit_code: Some(0),
    };

    let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
    let action = app.handle_key(key);

    // Unhandled key returns None, state unchanged
    assert_eq!(action, None);
    assert!(matches!(app.state, AppState::SessionTerminatedPopup { .. }));
}

#[test]
fn test_session_terminated_popup_key_keep() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.state = AppState::SessionTerminatedPopup {
        session_id: id,
        exit_code: Some(0),
    };

    let key = KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE);
    let action = app.handle_key(key);

    assert_eq!(action, None);
    assert_eq!(app.state, AppState::Normal);
}

#[test]
fn test_session_terminated_popup_key_esc() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.state = AppState::SessionTerminatedPopup {
        session_id: id,
        exit_code: Some(0),
    };

    let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
    let action = app.handle_key(key);

    assert_eq!(action, None);
    assert_eq!(app.state, AppState::Normal);
}

#[test]
fn test_close_session_only_for_terminated() {
    let (mut app, _rx) = create_test_app();
    let id_running = SessionId::new_v4();
    let id_terminated = SessionId::new_v4();

    app.sessions.push(SessionSnapshot {
        id: id_running,
        name: "running".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.sessions.push(SessionSnapshot {
        id: id_terminated,
        name: "terminated".to_string(),
        status: SessionStatus::Terminated { exit_code: Some(0) },
        branch: None,
    });

    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Sessions;

    // Select running session - 'd' should return None
    app.popup_state.session_list.select(Some(0));
    let key = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, None);

    // Select terminated session - 'd' should return CloseSession
    app.popup_state.session_list.select(Some(1));
    let key = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CloseSession { id: id_terminated }));
}

#[test]
fn test_session_section_unhandled_key() {
    let (mut app, _rx) = create_test_app();
    app.sessions.push(SessionSnapshot {
        id: SessionId::new_v4(),
        name: "test".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(0));

    // Unhandled key in session section
    let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

#[test]
fn test_worktree_section_up_key() {
    let (mut app, _rx) = create_test_app();
    app.worktrees.push(WorktreeItem::new(
        "branch-1",
        PathBuf::from("/path/1"),
        GitWorktreeStatus::default(),
    ));
    app.worktrees.push(WorktreeItem::new(
        "branch-2",
        PathBuf::from("/path/2"),
        GitWorktreeStatus::default(),
    ));
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Worktrees;
    app.popup_state.worktree_list.select(Some(1));

    // Up key in worktree section
    let key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::SelectPrev));
}

#[test]
fn test_worktree_section_unhandled_key() {
    let (mut app, _rx) = create_test_app();
    app.worktrees.push(WorktreeItem::new(
        "branch-1",
        PathBuf::from("/path/1"),
        GitWorktreeStatus::default(),
    ));
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Worktrees;
    app.popup_state.worktree_list.select(Some(0));

    // Unhandled key in worktree section
    let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

// === Phase 2: render() snapshot tests ===
mod render_snapshots {
    use super::*;
    use insta::assert_snapshot;
    use ratatui::{Terminal, backend::TestBackend};

    /// Render app to string buffer for snapshot testing
    fn render_app_to_string(app: &mut TuiApp, width: u16, height: u16) -> String {
        let backend = TestBackend::new(width, height);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| app.render(f)).unwrap();
        let buffer = terminal.backend().buffer().clone();
        crate::tui::test_utils::buffer_to_snapshot(&buffer)
    }

    #[test]
    fn render_normal_empty_sessions() {
        let (mut app, _rx) = create_test_app();
        let output = render_app_to_string(&mut app, 80, 24);
        assert_snapshot!(output);
    }

    #[test]
    fn render_normal_with_sessions() {
        let (mut app, _rx) = create_test_app_with_sessions(3);
        let output = render_app_to_string(&mut app, 80, 24);
        assert_snapshot!(output);
    }

    #[test]
    fn render_workspace_popup() {
        let (mut app, _rx) = create_test_app_with_sessions(2);
        app.state = AppState::WorkspacePopup;
        app.popup_state.section = PopupSection::Sessions;
        app.popup_state.session_list.select(Some(0));
        let output = render_app_to_string(&mut app, 80, 24);
        assert_snapshot!(output);
    }

    #[test]
    fn render_confirm_quit_popup() {
        let (mut app, _rx) = create_test_app();
        app.state = AppState::ConfirmQuit;
        app.quit_selected_yes = false;
        let output = render_app_to_string(&mut app, 80, 24);
        assert_snapshot!(output);
    }

    #[test]
    fn render_confirm_quit_yes_selected() {
        let (mut app, _rx) = create_test_app();
        app.state = AppState::ConfirmQuit;
        app.quit_selected_yes = true;
        let output = render_app_to_string(&mut app, 80, 24);
        assert_snapshot!(output);
    }

    #[test]
    fn render_error_popup() {
        let (mut app, _rx) = create_test_app();
        app.state = AppState::ErrorPopup {
            message: "Failed to create worktree: branch already exists".to_string(),
            from_popup: true,
        };
        let output = render_app_to_string(&mut app, 80, 24);
        assert_snapshot!(output);
    }

    #[test]
    fn render_session_terminated_popup() {
        let (mut app, _rx) = create_test_app();
        let id = SessionId::new_v4();
        app.sessions.push(SessionSnapshot {
            id,
            name: "feature-auth".to_string(),
            status: SessionStatus::Terminated { exit_code: Some(0) },
            branch: Some("feature/auth".to_string()),
        });
        app.terminal_buffers
            .insert(id, vt100::Parser::new(24, 80, 1000));
        app.state = AppState::SessionTerminatedPopup {
            session_id: id,
            exit_code: Some(0),
        };
        let output = render_app_to_string(&mut app, 80, 24);
        assert_snapshot!(output);
    }

    #[test]
    fn render_with_terminal_content() {
        let (mut app, _rx) = create_test_app();
        let id = SessionId::new_v4();
        app.handle_session_event(SessionEvent::Created {
            id,
            branch: None,
            auto_input: Vec::new(),
        });
        app.handle_session_event(SessionEvent::Output {
            id,
            data: b"$ ls\r\nfile1.txt\r\nfile2.txt".to_vec(),
        });
        let output = render_app_to_string(&mut app, 80, 24);
        assert_snapshot!(output);
    }

    #[test]
    fn render_status_bar_with_branch() {
        let (mut app, _rx) = create_test_app();
        let id = SessionId::new_v4();
        app.sessions.push(SessionSnapshot {
            id,
            name: "test-session".to_string(),
            status: SessionStatus::Running,
            branch: Some("feature/auth".to_string()),
        });
        app.terminal_buffers
            .insert(id, vt100::Parser::new(24, 80, 1000));
        let output = render_app_to_string(&mut app, 80, 24);
        // Branch is styled with Gray color, so each char has [Gy] prefix
        // Check for presence of branch name parts (styled)
        assert!(output.contains("[Gy]f"));
        assert!(output.contains("[Gy]a"));
        assert!(output.contains("[Gy]u"));
        assert!(output.contains("[Gy]t"));
        assert!(output.contains("[Gy]h"));
    }

    #[test]
    fn render_notifications_badge() {
        let (mut app, _rx) = create_test_app_with_sessions(1);
        // Mark 5 different sessions as pending
        for i in 0..5 {
            app.mark_pending(&SessionId::from_u128(i));
        }
        let output = render_app_to_string(&mut app, 80, 24);
        // Verify notification count visible
        assert!(output.contains('5'));
    }
}

// === Phase 3: SessionEvent handler tests ===

// WorktreesRefreshed tests
#[test]
fn test_worktrees_refreshed_updates_list() {
    let (mut app, _rx) = create_test_app();
    app.popup_state.loading = LoadingOperation::Fetching;

    let worktrees = vec![
        crate::worktree::WorktreeInfo {
            branch: "main".to_string(),
            path: PathBuf::from("/path/1"),
            status: GitWorktreeStatus::default(),
        },
        crate::worktree::WorktreeInfo {
            branch: "feature".to_string(),
            path: PathBuf::from("/path/2"),
            status: GitWorktreeStatus {
                dirty: true,
                ..Default::default()
            },
        },
    ];

    app.handle_session_event(SessionEvent::WorktreesRefreshed {
        worktrees,
        fetch_pending: false,
    });

    assert_eq!(app.worktrees.len(), 2);
    assert_eq!(app.popup_state.loading, LoadingOperation::Idle);
    assert_eq!(app.popup_state.worktree_list.selected(), Some(0));
}

#[test]
fn test_worktrees_refreshed_preserves_selection_when_valid() {
    let (mut app, _rx) = create_test_app_with_worktrees(3);
    app.popup_state.worktree_list.select(Some(1));

    let worktrees = vec![
        crate::worktree::WorktreeInfo {
            branch: "new-1".to_string(),
            path: PathBuf::from("/new/1"),
            status: GitWorktreeStatus::default(),
        },
        crate::worktree::WorktreeInfo {
            branch: "new-2".to_string(),
            path: PathBuf::from("/new/2"),
            status: GitWorktreeStatus::default(),
        },
    ];

    app.handle_session_event(SessionEvent::WorktreesRefreshed {
        worktrees,
        fetch_pending: false,
    });

    // Selection at 1 remains valid (2 items, index 1 valid)
    assert_eq!(app.popup_state.worktree_list.selected(), Some(1));
}

#[test]
fn test_worktrees_refreshed_empty_list() {
    let (mut app, _rx) = create_test_app();
    app.popup_state.worktree_list.select(Some(0));

    app.handle_session_event(SessionEvent::WorktreesRefreshed {
        worktrees: vec![],
        fetch_pending: false,
    });

    assert!(app.worktrees.is_empty());
    // Selection should remain (no auto-clear in current impl)
}

// WorktreeDeleted tests
#[test]
fn test_worktree_deleted_success_removes_from_list() {
    let (mut app, _rx) = create_test_app();
    let path = PathBuf::from("/test/worktree");
    app.worktrees.push(WorktreeItem::new(
        "branch",
        path.clone(),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.worktree_list.select(Some(0));
    app.popup_state.loading = LoadingOperation::Deleting { path: path.clone() };

    app.handle_session_event(SessionEvent::WorktreeDeleted {
        path,
        result: Ok(()),
    });

    assert!(app.worktrees.is_empty());
    assert_eq!(app.popup_state.loading, LoadingOperation::Idle);
    assert_eq!(app.popup_state.worktree_list.selected(), None);
}

#[test]
fn test_worktree_deleted_adjusts_selection_down() {
    let (mut app, _rx) = create_test_app_with_worktrees(3);
    let path = app.worktrees[2].path.clone();
    app.popup_state.worktree_list.select(Some(2)); // Last item selected
    app.popup_state.loading = LoadingOperation::Deleting { path: path.clone() };

    app.handle_session_event(SessionEvent::WorktreeDeleted {
        path,
        result: Ok(()),
    });

    assert_eq!(app.worktrees.len(), 2);
    assert_eq!(app.popup_state.worktree_list.selected(), Some(1)); // Adjusted to last valid
}

#[test]
fn test_worktree_deleted_error_shows_error_popup() {
    let (mut app, _rx) = create_test_app_with_worktrees(1);
    let path = app.worktrees[0].path.clone();
    app.popup_state.loading = LoadingOperation::Deleting { path: path.clone() };

    app.handle_session_event(SessionEvent::WorktreeDeleted {
        path,
        result: Err("Worktree has uncommitted changes".to_string()),
    });

    assert!(matches!(app.state, AppState::ErrorPopup { .. }));
    assert_eq!(app.worktrees.len(), 1); // Not removed
    assert_eq!(app.popup_state.loading, LoadingOperation::Idle);
}

#[test]
fn test_worktree_deleted_middle_item_selection_stays() {
    let (mut app, _rx) = create_test_app_with_worktrees(5);
    let path = app.worktrees[2].path.clone();
    app.popup_state.worktree_list.select(Some(1)); // Select item before deleted

    app.handle_session_event(SessionEvent::WorktreeDeleted {
        path,
        result: Ok(()),
    });

    assert_eq!(app.worktrees.len(), 4);
    assert_eq!(app.popup_state.worktree_list.selected(), Some(1)); // Unchanged
}

// HookReceived tests
#[test]
fn test_hook_received_notification_marks_session_pending() {
    let (mut app, _rx) = create_test_app();
    assert_eq!(app.pending_count(), 0);

    let event = create_test_hook_event(crate::hooks::HookEventType::Notification, true);
    let session_id = event.session_id;
    app.handle_session_event(SessionEvent::HookReceived { event });

    assert!(app.is_pending(&session_id));
    assert_eq!(app.pending_count(), 1);
}

#[test]
fn test_hook_received_non_attention_no_pending() {
    let (mut app, _rx) = create_test_app();

    // PreToolUse doesn't require attention
    let event = create_test_hook_event(crate::hooks::HookEventType::PreToolUse, false);
    let session_id = event.session_id;
    app.handle_session_event(SessionEvent::HookReceived { event });

    assert!(!app.is_pending(&session_id));
    assert_eq!(app.pending_count(), 0);
}

#[test]
fn test_hook_received_with_bell_enabled() {
    // Note: Can't test actual bell output, but can verify no panic
    let (cmd_tx, _rx) = mpsc::channel(16);
    let config = NotificationConfig {
        terminal_bell: true,
        ..NotificationConfig::default()
    };
    let mut app = TuiApp::new(cmd_tx, config, vec![]);

    let event = create_test_hook_event(crate::hooks::HookEventType::Notification, true);
    let session_id = event.session_id;
    app.handle_session_event(SessionEvent::HookReceived { event });

    assert!(app.is_pending(&session_id));
    assert_eq!(app.pending_count(), 1);
}

#[test]
fn test_hook_received_multiple_events_from_different_sessions() {
    let (mut app, _rx) = create_test_app();

    // Each event has a unique session_id (from new_v4())
    for _ in 0..5 {
        let event = create_test_hook_event(crate::hooks::HookEventType::Notification, true);
        app.handle_session_event(SessionEvent::HookReceived { event });
    }

    assert_eq!(app.pending_count(), 5);
}

#[test]
fn test_hook_received_same_session_idempotent() {
    let (mut app, _rx) = create_test_app();
    let session_id = SessionId::new_v4();

    // Multiple events from same session should only count as 1 pending
    for _ in 0..3 {
        app.mark_pending(&session_id);
    }

    assert_eq!(app.pending_count(), 1);
    assert!(app.is_pending(&session_id));
}

#[test]
fn test_output_does_not_clear_pending_status() {
    let (mut app, _rx) = create_test_app_with_sessions(1);
    let session_id = app.sessions[0].id;

    // Mark session as pending
    app.mark_pending(&session_id);
    assert!(app.is_pending(&session_id));

    // Output from session should NOT clear pending (only user input clears)
    app.handle_session_event(SessionEvent::Output {
        id: session_id,
        data: b"some output".to_vec(),
    });

    assert!(app.is_pending(&session_id));
    assert_eq!(app.pending_count(), 1);
}

#[test]
fn test_switch_session_does_not_clear_pending_status() {
    let (mut app, _rx) = create_test_app_with_sessions(2);
    let session_id_1 = app.sessions[1].id;

    // Start at session 0, mark session 1 as pending
    app.active_idx = 0;
    app.mark_pending(&session_id_1);
    assert!(app.is_pending(&session_id_1));

    // Switch to session 1 should NOT clear its pending status (only Enter clears)
    app.switch_session(1);

    assert!(app.is_pending(&session_id_1));
    assert_eq!(app.pending_count(), 1);
}

#[test]
fn test_next_session_does_not_clear_pending_status() {
    let (mut app, _rx) = create_test_app_with_sessions(2);
    let session_id_1 = app.sessions[1].id;

    // Start at session 0, mark session 1 as pending
    app.active_idx = 0;
    app.mark_pending(&session_id_1);

    // Next session should go to 1 but NOT clear its pending (only Enter clears)
    app.next_session();

    assert_eq!(app.active_idx, 1);
    assert!(app.is_pending(&session_id_1));
}

#[test]
fn test_prev_session_does_not_clear_pending_status() {
    let (mut app, _rx) = create_test_app_with_sessions(2);
    let session_id_0 = app.sessions[0].id;

    // Start at session 1, mark session 0 as pending
    app.active_idx = 1;
    app.mark_pending(&session_id_0);

    // Prev session should go to 0 but NOT clear its pending (only Enter clears)
    app.prev_session();

    assert_eq!(app.active_idx, 0);
    assert!(app.is_pending(&session_id_0));
}

#[tokio::test]
async fn test_send_input_with_enter_clears_pending() {
    let (mut app, _rx) = create_test_app_with_sessions(1);
    let session_id = app.sessions[0].id;

    // Mark session as pending
    app.mark_pending(&session_id);
    assert!(app.is_pending(&session_id));

    // Send input with Enter (\r) should clear pending for active session
    app.send_input(b"hello\r").await;

    assert!(!app.is_pending(&session_id));
    assert_eq!(app.pending_count(), 0);
}

#[tokio::test]
async fn test_send_input_with_newline_clears_pending() {
    let (mut app, _rx) = create_test_app_with_sessions(1);
    let session_id = app.sessions[0].id;

    // Mark session as pending
    app.mark_pending(&session_id);
    assert!(app.is_pending(&session_id));

    // Send input with newline (\n) should clear pending for active session
    app.send_input(b"hello\n").await;

    assert!(!app.is_pending(&session_id));
    assert_eq!(app.pending_count(), 0);
}

#[tokio::test]
async fn test_send_input_without_newline_does_not_clear_pending() {
    let (mut app, _rx) = create_test_app_with_sessions(1);
    let session_id = app.sessions[0].id;

    // Mark session as pending
    app.mark_pending(&session_id);
    assert!(app.is_pending(&session_id));

    // Send input without Enter/newline should NOT clear pending
    app.send_input(b"hello").await;

    assert!(app.is_pending(&session_id));
    assert_eq!(app.pending_count(), 1);
}

#[tokio::test]
async fn test_send_input_clears_pending_only_for_active_session() {
    let (mut app, _rx) = create_test_app_with_sessions(2);
    let session_id_0 = app.sessions[0].id;
    let session_id_1 = app.sessions[1].id;

    // Start at session 0, mark both as pending
    app.active_idx = 0;
    app.mark_pending(&session_id_0);
    app.mark_pending(&session_id_1);
    assert_eq!(app.pending_count(), 2);

    // Send Enter on session 0 should only clear session 0's pending
    app.send_input(b"\r").await;

    assert!(!app.is_pending(&session_id_0));
    assert!(app.is_pending(&session_id_1));
    assert_eq!(app.pending_count(), 1);
}

// === Phase 4: dispatch() async tests ===

#[tokio::test]
async fn test_dispatch_delete_worktree() {
    let (mut app, mut rx) = create_test_app_with_worktrees(1);
    let path = app.worktrees[0].path.clone();
    app.state = AppState::WorkspacePopup;

    app.dispatch(AppAction::DeleteWorktree { path: path.clone() })
        .await
        .unwrap();

    assert!(matches!(
        app.popup_state.loading,
        LoadingOperation::Deleting { .. }
    ));

    // Verify command sent
    if let Ok(SessionCommand::DeleteWorktreeAsync { path: cmd_path }) = rx.try_recv() {
        assert_eq!(cmd_path, path);
    } else {
        panic!("Expected DeleteWorktreeAsync command");
    }
}

#[tokio::test]
async fn test_dispatch_pull_worktree() {
    let (mut app, mut rx) = create_test_app_with_worktrees(1);
    let path = app.worktrees[0].path.clone();
    app.state = AppState::WorkspacePopup;

    app.dispatch(AppAction::PullWorktree { path: path.clone() })
        .await
        .unwrap();

    assert!(matches!(
        app.popup_state.loading,
        LoadingOperation::Pulling { .. }
    ));

    if let Ok(SessionCommand::PullWorktreeAsync { path: cmd_path }) = rx.try_recv() {
        assert_eq!(cmd_path, path);
    } else {
        panic!("Expected PullWorktreeAsync command");
    }
}

#[tokio::test]
async fn test_dispatch_close_session_success() {
    let (mut app, mut rx) = create_test_app();
    let id = SessionId::new_v4();
    app.sessions
        .push(create_terminated_session(id, "terminated"));
    app.terminal_buffers
        .insert(id, vt100::Parser::new(24, 80, 1000));
    app.scroll_offsets.insert(id, 5);
    app.popup_state.session_list.select(Some(0));

    let handle = tokio::spawn(async move {
        if let Some(SessionCommand::CloseSession {
            id: cmd_id,
            response_tx,
        }) = rx.recv().await
        {
            assert_eq!(cmd_id, id);
            let _ = response_tx.send(Ok(Some(PathBuf::from("/worktree/path"))));
        }
    });

    app.dispatch(AppAction::CloseSession { id }).await.unwrap();

    assert!(app.sessions.is_empty());
    assert!(!app.terminal_buffers.contains_key(&id));
    assert!(!app.scroll_offsets.contains_key(&id));
    handle.await.unwrap();
}

#[tokio::test]
async fn test_dispatch_close_session_clears_pending_status() {
    let (mut app, mut rx) = create_test_app();
    let id = SessionId::new_v4();
    app.sessions
        .push(create_terminated_session(id, "terminated"));
    app.terminal_buffers
        .insert(id, vt100::Parser::new(24, 80, 1000));
    // Mark session as pending
    app.pending_sessions.insert(id);
    assert!(app.is_pending(&id));

    let handle = tokio::spawn(async move {
        if let Some(SessionCommand::CloseSession { response_tx, .. }) = rx.recv().await {
            let _ = response_tx.send(Ok(None));
        }
    });

    app.dispatch(AppAction::CloseSession { id }).await.unwrap();

    // Pending status should be cleared
    assert!(!app.is_pending(&id));
    assert_eq!(app.pending_count(), 0);
    handle.await.unwrap();
}

#[tokio::test]
async fn test_dispatch_close_session_adjusts_active_idx() {
    let (mut app, mut rx) = create_test_app();
    let id1 = SessionId::new_v4();
    let id2 = SessionId::new_v4();
    app.sessions.push(create_terminated_session(id1, "s1"));
    app.sessions.push(create_terminated_session(id2, "s2"));
    app.terminal_buffers
        .insert(id1, vt100::Parser::new(24, 80, 1000));
    app.terminal_buffers
        .insert(id2, vt100::Parser::new(24, 80, 1000));
    app.active_idx = 1; // Second session active

    let handle = tokio::spawn(async move {
        if let Some(SessionCommand::CloseSession { response_tx, .. }) = rx.recv().await {
            let _ = response_tx.send(Ok(None));
        }
    });

    app.dispatch(AppAction::CloseSession { id: id2 })
        .await
        .unwrap(); // Remove active

    assert_eq!(app.active_idx, 0); // Adjusted
    assert_eq!(app.sessions.len(), 1);
    handle.await.unwrap();
}

#[tokio::test]
async fn test_dispatch_close_session_error() {
    let (mut app, mut rx) = create_test_app();
    let id = SessionId::new_v4();
    app.sessions.push(create_terminated_session(id, "test"));

    let handle = tokio::spawn(async move {
        if let Some(SessionCommand::CloseSession { response_tx, .. }) = rx.recv().await {
            let _ = response_tx.send(Err(SessionError::NotFound { id: id.to_string() }));
        }
    });

    app.dispatch(AppAction::CloseSession { id }).await.unwrap();

    assert!(matches!(app.state, AppState::ErrorPopup { .. }));
    handle.await.unwrap();
}

#[tokio::test]
async fn test_dispatch_close_session_adjusts_popup_selection() {
    let (mut app, mut rx) = create_test_app();
    for i in 0..3 {
        let id = SessionId::new_v4();
        app.sessions
            .push(create_terminated_session(id, &format!("s{i}")));
        app.terminal_buffers
            .insert(id, vt100::Parser::new(24, 80, 1000));
    }
    app.popup_state.session_list.select(Some(2)); // Select last
    let id_to_close = app.sessions[2].id;

    let handle = tokio::spawn(async move {
        if let Some(SessionCommand::CloseSession { response_tx, .. }) = rx.recv().await {
            let _ = response_tx.send(Ok(None));
        }
    });

    app.dispatch(AppAction::CloseSession { id: id_to_close })
        .await
        .unwrap();

    assert_eq!(app.popup_state.session_list.selected(), Some(1)); // Adjusted
    handle.await.unwrap();
}

// === Phase 5: Edge case tests ===

#[tokio::test]
async fn test_dispatch_terminate_current_session_no_sessions() {
    let (mut app, _rx) = create_test_app();
    // No sessions - should not error
    app.dispatch(AppAction::TerminateCurrentSession)
        .await
        .unwrap();
}

// === Phase 6: default_args tests ===

#[test]
fn test_new_stores_default_args() {
    let (cmd_tx, _rx) = mpsc::channel(16);
    let args = vec!["--dangerously-skip-permissions".to_string()];
    let app = TuiApp::new(cmd_tx, NotificationConfig::default(), args.clone());
    assert_eq!(app.default_args(), &args);
}

#[tokio::test]
async fn test_create_session_uses_default_args() {
    let (cmd_tx, mut cmd_rx) = mpsc::channel(16);
    let args = vec!["--model".to_string(), "opus".to_string()];
    let mut app = TuiApp::new(cmd_tx, NotificationConfig::default(), args.clone());
    app.set_size(24, 80);

    let handle = tokio::spawn(async move {
        match cmd_rx.recv().await {
            Some(SessionCommand::Create {
                args, response_tx, ..
            }) => {
                let _ = response_tx.send(Ok(SessionId::new_v4()));
                args
            }
            _ => vec![],
        }
    });

    let _ = app.create_session_with_branch(None).await;
    let received_args = handle.await.unwrap();
    assert_eq!(received_args, args);
}

#[tokio::test]
async fn test_adopt_worktree_uses_default_args() {
    let (cmd_tx, mut cmd_rx) = mpsc::channel(16);
    let args = vec!["--dangerously-skip-permissions".to_string()];
    let mut app = TuiApp::new(cmd_tx, NotificationConfig::default(), args.clone());
    app.set_size(24, 80);

    let handle = tokio::spawn(async move {
        match cmd_rx.recv().await {
            Some(SessionCommand::CreateFromWorktree {
                args, response_tx, ..
            }) => {
                let _ = response_tx.send(Ok(SessionId::new_v4()));
                args
            }
            _ => vec![],
        }
    });

    let _ = app.adopt_worktree(Path::new("/tmp/test")).await;
    let received_args = handle.await.unwrap();
    assert_eq!(received_args, args);
}

// === Phase 7: GitHub Issue Integration tests (PR #94) ===
// LoadingOperation tests moved to popup/state.rs with parametrization

#[test]
fn test_handle_key_action_select_popup_esc() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices: vec![],
    };

    let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CancelIssueFlow));
}

#[test]
fn test_handle_key_action_select_popup_ctrl_g() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices: vec![],
    };

    let key = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CancelIssueFlow));
}

#[test]
fn test_handle_key_action_select_popup_enter_with_selection() {
    use crate::github::{ActionChoice, ActionType};

    let (mut app, _rx) = create_test_app();
    let choices = vec![
        ActionChoice {
            branch: "feat/test".to_string(),
            action: ActionType::Implement,
            prompt: "Test #42".to_string(),
        },
        ActionChoice {
            branch: "survey/test".to_string(),
            action: ActionType::Survey,
            prompt: "Survey #42".to_string(),
        },
    ];
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices,
    };
    app.action_select_state.list_state.select(Some(0));

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::SelectActionChoice { index: 0 }));
}

#[test]
fn test_handle_key_action_select_popup_enter_no_selection() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices: vec![],
    };
    // No selection

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

#[test]
fn test_handle_key_confirm_permissions_y() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: false,
    };

    let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::ConfirmDangerousPermissions));
}

#[test]
fn test_handle_key_confirm_permissions_n() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: true,
    };

    let key = KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CancelIssueFlow));
}

#[test]
fn test_handle_key_confirm_permissions_esc() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: true,
    };

    let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CancelIssueFlow));
}

#[test]
fn test_handle_key_confirm_permissions_ctrl_m_yes() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: true,
    };

    // Ctrl+m confirms when Yes is selected
    let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::ConfirmDangerousPermissions));
}

#[test]
fn test_handle_key_confirm_permissions_ctrl_m_no() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: false,
    };

    // Ctrl+m cancels when No is selected
    let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CancelIssueFlow));
}

#[test]
fn test_handle_key_issue_loading_ctrl_g() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::FetchingIssue { issue_number: 42 },
    };

    let key = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CancelIssueFlow));
}

#[tokio::test]
async fn test_dispatch_toggle_permissions_choice() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: false,
    };

    app.dispatch(AppAction::TogglePermissionsChoice)
        .await
        .unwrap();

    if let AppState::ConfirmPermissions { selected_yes, .. } = app.state {
        assert!(selected_yes);
    } else {
        panic!("Expected ConfirmPermissions state");
    }
}

// CancelIssueFlow returns to WorkspacePopup from any issue flow state
#[rstest]
#[case::from_issue_loading(AppState::IssueLoading { issue_number: 42, phase: LoadingOperation::FetchingIssue { issue_number: 42 } })]
#[case::from_action_select(AppState::ActionSelectPopup { issue_number: 42, choices: vec![] })]
#[case::from_confirm_permissions(AppState::ConfirmPermissions { branch: "feat/test".to_string(), prompt: "Test".to_string(), selected_yes: true })]
#[tokio::test]
async fn cancel_issue_flow_returns_to_workspace(#[case] initial_state: AppState) {
    let (mut app, _rx) = create_test_app();
    app.state = initial_state;
    app.dispatch(AppAction::CancelIssueFlow).await.unwrap();
    assert_eq!(app.state, AppState::WorkspacePopup);
}

#[tokio::test]
async fn test_dispatch_select_issue() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;

    app.dispatch(AppAction::SelectIssue { number: 42 })
        .await
        .unwrap();

    // Should transition to IssueLoading
    assert!(matches!(
        app.state,
        AppState::IssueLoading {
            issue_number: 42,
            ..
        }
    ));
}

#[test]
fn test_app_state_issue_loading_equality() {
    let state1 = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::FetchingIssue { issue_number: 42 },
    };
    let state2 = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::FetchingIssue { issue_number: 42 },
    };
    assert_eq!(state1, state2);
}

#[test]
fn test_app_state_action_select_popup_equality() {
    use crate::github::{ActionChoice, ActionType};

    let choices = vec![ActionChoice {
        branch: "feat/test".to_string(),
        action: ActionType::Implement,
        prompt: "Test".to_string(),
    }];

    let state1 = AppState::ActionSelectPopup {
        issue_number: 42,
        choices: choices.clone(),
    };
    let state2 = AppState::ActionSelectPopup {
        issue_number: 42,
        choices,
    };
    assert_eq!(state1, state2);
}

#[tokio::test]
async fn test_dispatch_select_action_choice() {
    use crate::github::{ActionChoice, ActionType};

    let (mut app, _rx) = create_test_app();
    let choices = vec![
        ActionChoice {
            branch: "feat/issue-42".to_string(),
            action: ActionType::Implement,
            prompt: "Implement #42".to_string(),
        },
        ActionChoice {
            branch: "survey/issue-42".to_string(),
            action: ActionType::Survey,
            prompt: "Survey #42".to_string(),
        },
    ];
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices,
    };

    app.dispatch(AppAction::SelectActionChoice { index: 0 })
        .await
        .unwrap();

    // Should transition to ConfirmPermissions with first choice
    if let AppState::ConfirmPermissions {
        branch,
        prompt,
        selected_yes,
    } = &app.state
    {
        assert_eq!(branch, "feat/issue-42");
        assert_eq!(prompt, "Implement #42");
        assert!(!selected_yes);
    } else {
        panic!("Expected ConfirmPermissions state");
    }
}

#[tokio::test]
async fn test_dispatch_select_action_choice_out_of_bounds() {
    use crate::github::{ActionChoice, ActionType};

    let (mut app, _rx) = create_test_app();
    let choices = vec![ActionChoice {
        branch: "feat/test".to_string(),
        action: ActionType::Implement,
        prompt: "Test".to_string(),
    }];
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices,
    };

    // Out of bounds index should not crash
    app.dispatch(AppAction::SelectActionChoice { index: 10 })
        .await
        .unwrap();

    // State should remain unchanged
    assert!(matches!(app.state, AppState::ActionSelectPopup { .. }));
}

#[tokio::test]
async fn test_dispatch_confirm_dangerous_permissions() {
    let (mut app, mut rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/issue-42".to_string(),
        prompt: "Implement #42".to_string(),
        selected_yes: true,
    };

    // Spawn handler for CreateWithAutoInput command
    let handle = tokio::spawn(async move {
        if let Some(SessionCommand::CreateWithAutoInput {
            branch_name,
            auto_input,
            response_tx,
            ..
        }) = rx.recv().await
        {
            assert_eq!(branch_name, Some("feat/issue-42".to_string()));
            assert!(!auto_input.is_empty());
            let _ = response_tx.send(Ok(SessionId::new_v4()));
        }
    });

    app.dispatch(AppAction::ConfirmDangerousPermissions)
        .await
        .unwrap();

    // State should transition to Normal
    assert_eq!(app.state, AppState::Normal);
    handle.await.unwrap();
}

#[tokio::test]
async fn test_dispatch_action_select_navigation() {
    use crate::github::{ActionChoice, ActionType};

    let (mut app, _rx) = create_test_app();
    let choices = vec![
        ActionChoice {
            branch: "feat/1".to_string(),
            action: ActionType::Implement,
            prompt: "1".to_string(),
        },
        ActionChoice {
            branch: "feat/2".to_string(),
            action: ActionType::Implement,
            prompt: "2".to_string(),
        },
        ActionChoice {
            branch: "feat/3".to_string(),
            action: ActionType::Implement,
            prompt: "3".to_string(),
        },
    ];
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices,
    };
    app.action_select_state.list_state.select(Some(0));

    // SelectNext
    app.dispatch(AppAction::SelectNext).await.unwrap();
    assert_eq!(app.action_select_state.selected(), Some(1));

    // SelectPrev
    app.dispatch(AppAction::SelectPrev).await.unwrap();
    assert_eq!(app.action_select_state.selected(), Some(0));
}

// === Phase 7: Additional Issue Section Tests (Coverage Improvement) ===

/// Helper: create app with pre-populated issues
#[allow(clippy::cast_possible_truncation)]
fn create_test_app_with_issues(count: usize) -> (TuiApp, mpsc::Receiver<SessionCommand>) {
    let (mut app, rx) = create_test_app();
    for i in 0..count {
        app.issues.push(IssueItem::new(
            (i + 1) as u32,
            format!("Issue {}", i + 1),
            "",
        ));
    }
    if !app.issues.is_empty() {
        app.popup_state.issue_list.select(Some(0));
    }
    (app, rx)
}

#[test]
fn test_handle_key_issue_section_down() {
    let (mut app, _rx) = create_test_app_with_issues(3);
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Issues;

    let key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::SelectNext));
}

#[test]
fn test_handle_key_issue_section_up() {
    let (mut app, _rx) = create_test_app_with_issues(3);
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(2));

    let key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::SelectPrev));
}

#[test]
fn test_handle_key_issue_section_enter_with_selection() {
    let (mut app, _rx) = create_test_app_with_issues(3);
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(1));

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::SelectIssue { number: 2 }));
}

#[test]
fn test_handle_key_issue_section_enter_no_selection() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Issues;
    // No issues, no selection

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

#[test]
fn test_handle_key_issue_section_unhandled() {
    let (mut app, _rx) = create_test_app_with_issues(1);
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Issues;

    let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

#[test]
fn test_select_next_issues_section() {
    let (mut app, _rx) = create_test_app_with_issues(3);
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(0));

    app.select_next();
    assert_eq!(app.popup_state.issue_list.selected(), Some(1));

    app.select_next();
    assert_eq!(app.popup_state.issue_list.selected(), Some(2));

    // Wrap around
    app.select_next();
    assert_eq!(app.popup_state.issue_list.selected(), Some(0));
}

#[test]
fn test_select_prev_issues_section() {
    let (mut app, _rx) = create_test_app_with_issues(3);
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(0));

    // Wrap around backward
    app.select_prev();
    assert_eq!(app.popup_state.issue_list.selected(), Some(2));

    app.select_prev();
    assert_eq!(app.popup_state.issue_list.selected(), Some(1));
}

// Navigation on empty issues section does nothing
#[test]
fn issues_nav_empty_noop() {
    let (mut app, _rx) = create_test_app();
    app.popup_state.section = PopupSection::Issues;
    app.select_next();
    assert_eq!(app.popup_state.issue_list.selected(), None);
    app.select_prev();
    assert_eq!(app.popup_state.issue_list.selected(), None);
}

// === IssuesFetched Event Tests ===

#[test]
fn test_issues_fetched_success() {
    use crate::github::{GitHubIssue, IssueState};

    let (mut app, _rx) = create_test_app();
    let issues = vec![
        GitHubIssue {
            number: 1,
            title: "First issue".to_string(),
            body: String::new(),
            labels: vec![],
            state: IssueState::Open,
        },
        GitHubIssue {
            number: 2,
            title: "Second issue".to_string(),
            body: "Body".to_string(),
            labels: vec![],
            state: IssueState::Open,
        },
    ];

    app.handle_session_event(SessionEvent::IssuesFetched { result: Ok(issues) });

    assert_eq!(app.issues.len(), 2);
    assert_eq!(app.issues[0].number, 1);
    assert_eq!(app.issues[1].number, 2);
    assert_eq!(app.popup_state.issue_list.selected(), Some(0));
}

#[test]
fn test_issues_fetched_empty() {
    let (mut app, _rx) = create_test_app();

    app.handle_session_event(SessionEvent::IssuesFetched { result: Ok(vec![]) });

    assert!(app.issues.is_empty());
    assert_eq!(app.popup_state.issue_list.selected(), None);
}

#[test]
fn test_issues_fetched_error() {
    let (mut app, _rx) = create_test_app();

    app.handle_session_event(SessionEvent::IssuesFetched {
        result: Err("Network error".to_string()),
    });

    // Should not crash, issues remain empty
    assert!(app.issues.is_empty());
}

// === IssueFetched Event Tests ===

#[test]
fn test_issue_fetched_transitions_to_generating() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::FetchingIssue { issue_number: 42 },
    };

    app.handle_session_event(SessionEvent::IssueFetched { issue_number: 42 });

    assert!(matches!(
        app.state,
        AppState::IssueLoading {
            issue_number: 42,
            phase: LoadingOperation::GeneratingActions { .. }
        }
    ));
}

#[test]
fn test_issue_fetched_wrong_issue_number_ignored() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::FetchingIssue { issue_number: 42 },
    };

    // Different issue number should be ignored
    app.handle_session_event(SessionEvent::IssueFetched { issue_number: 99 });

    // State unchanged
    assert!(matches!(
        app.state,
        AppState::IssueLoading {
            issue_number: 42,
            phase: LoadingOperation::FetchingIssue { .. }
        }
    ));
}

#[test]
fn test_issue_fetched_not_in_loading_state() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::Normal;

    // Should not crash when not in loading state
    app.handle_session_event(SessionEvent::IssueFetched { issue_number: 42 });

    assert_eq!(app.state, AppState::Normal);
}

// === IssueActionsFetched Event Tests ===

#[test]
fn test_issue_actions_fetched_success() {
    use crate::github::{ActionChoice, ActionType};

    let (mut app, _rx) = create_test_app();
    app.state = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::GeneratingActions { issue_number: 42 },
    };

    let choices = vec![
        ActionChoice {
            branch: "feat/issue-42".to_string(),
            action: ActionType::Implement,
            prompt: "Implement #42".to_string(),
        },
        ActionChoice {
            branch: "survey/issue-42".to_string(),
            action: ActionType::Survey,
            prompt: "Survey #42".to_string(),
        },
    ];

    app.handle_session_event(SessionEvent::IssueActionsFetched {
        issue_number: 42,
        result: Ok(choices),
    });

    if let AppState::ActionSelectPopup {
        issue_number,
        choices,
    } = &app.state
    {
        assert_eq!(*issue_number, 42);
        assert_eq!(choices.len(), 2);
    } else {
        panic!("Expected ActionSelectPopup state");
    }
    assert_eq!(app.action_select_state.selected(), Some(0));
}

#[test]
fn test_issue_actions_fetched_empty() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::GeneratingActions { issue_number: 42 },
    };

    app.handle_session_event(SessionEvent::IssueActionsFetched {
        issue_number: 42,
        result: Ok(vec![]),
    });

    // Shows error popup with from_popup=true (IssueLoading → ErrorPopup)
    assert!(matches!(
        app.state,
        AppState::ErrorPopup {
            from_popup: true,
            ..
        }
    ));
}

#[test]
fn test_issue_actions_fetched_error() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::GeneratingActions { issue_number: 42 },
    };

    app.handle_session_event(SessionEvent::IssueActionsFetched {
        issue_number: 42,
        result: Err("Claude API error".to_string()),
    });

    // Shows error popup with from_popup=true (IssueLoading → ErrorPopup)
    assert!(matches!(
        app.state,
        AppState::ErrorPopup {
            from_popup: true,
            ..
        }
    ));
}

// === Cross-Section Navigation with Issues ===

#[test]
fn test_cross_section_next_from_branch_input_to_issues_only() {
    let (mut app, _rx) = create_test_app_with_issues(2);
    app.sessions.clear();
    app.worktrees.clear();
    app.popup_state.section = PopupSection::BranchInput;

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::Issues);
    assert_eq!(app.popup_state.issue_list.selected(), Some(0));
}

#[test]
fn test_cross_section_next_from_sessions_to_issues_no_worktrees() {
    let (mut app, _rx) = create_test_app_with_issues(2);
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-1".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.worktrees.clear();
    app.popup_state.section = PopupSection::Sessions;
    app.popup_state.session_list.select(Some(0));

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::Issues);
    assert_eq!(app.popup_state.issue_list.selected(), Some(0));
}

#[test]
fn test_cross_section_next_from_worktrees_to_issues() {
    let (mut app, _rx) = create_test_app_with_issues(2);
    app.worktrees.push(WorktreeItem::new(
        "branch",
        PathBuf::from("/path"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.section = PopupSection::Worktrees;
    app.popup_state.worktree_list.select(Some(0));

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::Issues);
    assert_eq!(app.popup_state.issue_list.selected(), Some(0));
}

#[test]
fn test_cross_section_next_within_issues() {
    let (mut app, _rx) = create_test_app_with_issues(3);
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(0));

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::Issues);
    assert_eq!(app.popup_state.issue_list.selected(), Some(1));
}

#[test]
fn test_cross_section_next_issues_wrap_to_branch_input() {
    let (mut app, _rx) = create_test_app_with_issues(2);
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(1)); // Last issue

    app.cross_section_next();

    assert_eq!(app.popup_state.section, PopupSection::BranchInput);
}

#[test]
fn test_cross_section_prev_from_branch_input_to_issues() {
    let (mut app, _rx) = create_test_app_with_issues(3);
    app.popup_state.section = PopupSection::BranchInput;

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::Issues);
    assert_eq!(app.popup_state.issue_list.selected(), Some(2)); // Last issue
}

#[test]
fn test_cross_section_prev_within_issues() {
    let (mut app, _rx) = create_test_app_with_issues(3);
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(2));

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::Issues);
    assert_eq!(app.popup_state.issue_list.selected(), Some(1));
}

#[test]
fn test_cross_section_prev_issues_to_worktrees() {
    let (mut app, _rx) = create_test_app_with_issues(2);
    app.worktrees.push(WorktreeItem::new(
        "branch-1",
        PathBuf::from("/path/1"),
        GitWorktreeStatus::default(),
    ));
    app.worktrees.push(WorktreeItem::new(
        "branch-2",
        PathBuf::from("/path/2"),
        GitWorktreeStatus::default(),
    ));
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(0));

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::Worktrees);
    assert_eq!(app.popup_state.worktree_list.selected(), Some(1)); // Last worktree
}

#[test]
fn test_cross_section_prev_issues_to_sessions_no_worktrees() {
    let (mut app, _rx) = create_test_app_with_issues(2);
    app.sessions.push(SessionSnapshot {
        id: uuid::Uuid::new_v4(),
        name: "session-1".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.worktrees.clear();
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(0));

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::Sessions);
    assert_eq!(app.popup_state.session_list.selected(), Some(0));
}

#[test]
fn test_cross_section_prev_issues_to_branch_input_no_worktrees_no_sessions() {
    let (mut app, _rx) = create_test_app_with_issues(2);
    app.sessions.clear();
    app.worktrees.clear();
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(0));

    app.cross_section_prev();

    assert_eq!(app.popup_state.section, PopupSection::BranchInput);
}

#[test]
fn test_handle_enter_for_section_issues() {
    let (mut app, _rx) = create_test_app_with_issues(2);
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(0));

    let action = app.handle_enter_for_section();

    assert_eq!(action, Some(AppAction::SelectIssue { number: 1 }));
}

#[test]
fn test_handle_enter_for_section_issues_no_selection() {
    let (mut app, _rx) = create_test_app();
    app.popup_state.section = PopupSection::Issues;
    // No issues

    let action = app.handle_enter_for_section();

    assert_eq!(action, None);
}

#[test]
fn test_workspace_popup_ctrl_m_confirm_in_issues() {
    let (mut app, _rx) = create_test_app_with_issues(2);
    app.state = AppState::WorkspacePopup;
    app.popup_state.section = PopupSection::Issues;
    app.popup_state.issue_list.select(Some(1));

    let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);

    assert_eq!(action, Some(AppAction::SelectIssue { number: 2 }));
}

// === Phase 8: Additional Coverage Tests ===

#[test]
fn test_handle_key_issue_loading_esc() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::FetchingIssue { issue_number: 42 },
    };

    let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CancelIssueFlow));
}

#[test]
fn test_handle_key_issue_loading_unhandled() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::IssueLoading {
        issue_number: 42,
        phase: LoadingOperation::FetchingIssue { issue_number: 42 },
    };

    let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

#[test]
fn test_handle_key_action_select_popup_ctrl_m() {
    use crate::github::{ActionChoice, ActionType};

    let (mut app, _rx) = create_test_app();
    let choices = vec![ActionChoice {
        branch: "feat/issue-42".to_string(),
        action: ActionType::Implement,
        prompt: "Implement #42".to_string(),
    }];
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices,
    };
    app.action_select_state.list_state.select(Some(0));

    let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::SelectActionChoice { index: 0 }));
}

#[test]
fn test_handle_key_action_select_popup_ctrl_m_no_selection() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices: vec![],
    };

    let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

#[test]
fn test_handle_key_action_select_popup_unhandled_char() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices: vec![],
    };

    let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

#[rstest]
#[case::tab(KeyCode::Tab)]
#[case::left(KeyCode::Left)]
#[case::right(KeyCode::Right)]
fn test_handle_key_confirm_permissions_toggle_keys(#[case] key_code: KeyCode) {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: false,
    };
    let key = KeyEvent::new(key_code, KeyModifiers::NONE);
    assert_eq!(
        app.handle_key(key),
        Some(AppAction::TogglePermissionsChoice)
    );
}

#[test]
fn test_handle_key_confirm_permissions_unhandled_char() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: false,
    };

    let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, None);
}

#[rstest]
#[case::up_arrow(KeyCode::Up, KeyModifiers::NONE, AppAction::SelectPrev)]
#[case::down_arrow(KeyCode::Down, KeyModifiers::NONE, AppAction::SelectNext)]
#[case::vim_j(KeyCode::Char('j'), KeyModifiers::NONE, AppAction::SelectNext)]
#[case::vim_k(KeyCode::Char('k'), KeyModifiers::NONE, AppAction::SelectPrev)]
#[case::emacs_ctrl_n(KeyCode::Char('n'), KeyModifiers::CONTROL, AppAction::SelectNext)]
#[case::emacs_ctrl_p(KeyCode::Char('p'), KeyModifiers::CONTROL, AppAction::SelectPrev)]
fn test_handle_key_action_select_popup_navigation(
    #[case] key_code: KeyCode,
    #[case] modifiers: KeyModifiers,
    #[case] expected: AppAction,
) {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices: vec![],
    };
    let key = KeyEvent::new(key_code, modifiers);
    assert_eq!(app.handle_key(key), Some(expected));
}

#[test]
fn test_handle_key_action_select_enter_with_selection() {
    use crate::github::{ActionChoice, ActionType};

    let (mut app, _rx) = create_test_app();
    let choices = vec![
        ActionChoice {
            branch: "feat/issue-1".to_string(),
            action: ActionType::Implement,
            prompt: "First".to_string(),
        },
        ActionChoice {
            branch: "feat/issue-2".to_string(),
            action: ActionType::Survey,
            prompt: "Second".to_string(),
        },
    ];
    app.state = AppState::ActionSelectPopup {
        issue_number: 42,
        choices,
    };
    app.action_select_state.list_state.select(Some(1));

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::SelectActionChoice { index: 1 }));
}

#[test]
fn test_confirm_permissions_ctrl_enter_when_yes() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: true,
    };

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::ConfirmDangerousPermissions));
}

#[test]
fn test_confirm_permissions_ctrl_enter_when_no() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: false,
    };

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL);
    let action = app.handle_key(key);
    assert_eq!(action, Some(AppAction::CancelIssueFlow));
}

#[test]
fn test_confirm_permissions_plain_enter_unhandled() {
    let (mut app, _rx) = create_test_app();
    app.state = AppState::ConfirmPermissions {
        branch: "feat/test".to_string(),
        prompt: "Test".to_string(),
        selected_yes: true,
    };

    let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
    let action = app.handle_key(key);
    // Plain Enter without Ctrl is unhandled
    assert_eq!(action, None);
}

// === Scroll position tests ===

#[test]
fn test_scroll_offset_preserved_on_output() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.sessions.push(SessionSnapshot {
        id,
        name: "test".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.terminal_buffers
        .insert(id, vt100::Parser::new(24, 80, 1000));
    app.active_idx = 0;

    // User scrolls up
    app.scroll_offsets.insert(id, 5);

    // New output arrives
    app.handle_session_event(SessionEvent::Output {
        id,
        data: b"new line\n".to_vec(),
    });

    // Scroll offset should be preserved (not reset to 0)
    assert_eq!(app.scroll_offsets.get(&id).copied(), Some(5));
}

#[test]
fn test_scroll_follow_mode_default() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.sessions.push(SessionSnapshot {
        id,
        name: "test".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.terminal_buffers
        .insert(id, vt100::Parser::new(24, 80, 1000));
    app.active_idx = 0;

    // No scroll offset set (follow mode)
    app.handle_session_event(SessionEvent::Output {
        id,
        data: b"output\n".to_vec(),
    });

    // Should remain in follow mode (offset 0 or absent)
    assert!(app.scroll_offsets.get(&id).copied().unwrap_or(0) == 0);
}

#[test]
fn test_scroll_up_down() {
    let (mut app, _rx) = create_test_app();
    let id = SessionId::new_v4();
    app.sessions.push(SessionSnapshot {
        id,
        name: "test".to_string(),
        status: SessionStatus::Running,
        branch: None,
    });
    app.terminal_buffers
        .insert(id, vt100::Parser::new(24, 80, 1000));
    app.active_idx = 0;

    // Scroll up (negative delta)
    app.scroll(-3);
    assert_eq!(app.scroll_offsets.get(&id).copied(), Some(3));

    // Scroll down (positive delta)
    app.scroll(2);
    assert_eq!(app.scroll_offsets.get(&id).copied(), Some(1));

    // Scroll down past bottom
    app.scroll(10);
    assert_eq!(app.scroll_offsets.get(&id).copied(), Some(0));
}

#[tokio::test]
async fn test_dispatch_show_workspace_selects_cached_issues() {
    let (mut app, _rx) = create_test_app();
    // Pre-populate cached issues
    for i in 1..=3_u32 {
        app.issues.push(IssueItem::new(i, format!("Issue {i}"), ""));
    }

    app.dispatch(AppAction::ShowWorkspacePopup).await.unwrap();

    assert_eq!(app.state, AppState::WorkspacePopup);
    assert_eq!(app.popup_state.issue_list.selected(), Some(0));
}
