//! Tests for `SessionManager`.
//!
//! Contains tests for session lifecycle, worktree operations, and async handlers.

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

use super::*;
use crate::github::mock::MockGitHubClient;
use crate::session::pty::mock::MockPty;
use crate::worktree::mock::MockWorktreeManager;
use rstest::rstest;

type TestManager = SessionManager<MockPty, MockWorktreeManager, MockGitHubClient>;

/// Worktree operation types for parametrized testing
#[derive(Debug, Clone, Copy)]
enum WorktreeOp {
    Delete,
    Pull,
    Fetch,
    Status,
}

fn create_test_manager(pty: MockPty, event_tx: mpsc::Sender<SessionEvent>) -> TestManager {
    SessionManager::new(
        10,
        pty,
        None,
        false,
        PullStrategy::default(),
        event_tx,
        MockGitHubClient::new(),
    )
}

fn create_test_manager_with_limit(
    max: usize,
    pty: MockPty,
    event_tx: mpsc::Sender<SessionEvent>,
) -> TestManager {
    SessionManager::new(
        max,
        pty,
        None,
        false,
        PullStrategy::default(),
        event_tx,
        MockGitHubClient::new(),
    )
}

fn create_test_manager_with_worktree(
    pty: MockPty,
    worktree: MockWorktreeManager,
    event_tx: mpsc::Sender<SessionEvent>,
) -> TestManager {
    SessionManager::new(
        10,
        pty,
        Some(worktree),
        false,
        PullStrategy::default(),
        event_tx,
        MockGitHubClient::new(),
    )
}

/// Send worktree command and get result
async fn send_worktree_command(
    cmd_tx: &mpsc::Sender<SessionCommand>,
    op: WorktreeOp,
    path: PathBuf,
) -> Result<(), SessionError> {
    match op {
        WorktreeOp::Delete => {
            let (response_tx, response_rx) = oneshot::channel();
            cmd_tx
                .send(SessionCommand::DeleteWorktree { path, response_tx })
                .await
                .expect("send failed");
            response_rx.await.expect("response failed")
        }
        WorktreeOp::Pull => {
            let (response_tx, response_rx) = oneshot::channel();
            cmd_tx
                .send(SessionCommand::PullWorktree { path, response_tx })
                .await
                .expect("send failed");
            response_rx.await.expect("response failed")
        }
        WorktreeOp::Fetch => {
            let (response_tx, response_rx) = oneshot::channel();
            cmd_tx
                .send(SessionCommand::FetchWorktree { path, response_tx })
                .await
                .expect("send failed");
            response_rx.await.expect("response failed")
        }
        WorktreeOp::Status => {
            let (response_tx, response_rx) = oneshot::channel();
            cmd_tx
                .send(SessionCommand::GetWorktreeStatus { path, response_tx })
                .await
                .expect("send failed");
            response_rx.await.expect("response failed").map(|_| ())
        }
    }
}

#[rstest]
#[case(WorktreeOp::Delete)]
#[case(WorktreeOp::Pull)]
#[case(WorktreeOp::Fetch)]
#[case(WorktreeOp::Status)]
#[tokio::test]
async fn worktree_op_success(#[case] op: WorktreeOp) {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let (worktree_path, _branch) = wt
        .create_worktree(Some("test-branch"))
        .expect("create worktree");
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    let result = send_worktree_command(&cmd_tx, op, worktree_path).await;
    assert!(result.is_ok(), "{op:?} should succeed: {result:?}");
}

#[rstest]
#[case(WorktreeOp::Delete)]
#[case(WorktreeOp::Pull)]
#[case(WorktreeOp::Fetch)]
#[case(WorktreeOp::Status)]
#[tokio::test]
async fn worktree_op_not_found(#[case] op: WorktreeOp) {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    let result = send_worktree_command(&cmd_tx, op, PathBuf::from("/nonexistent/path")).await;
    assert!(result.is_err(), "{op:?} should fail for non-existent path");
}

#[rstest]
#[case(WorktreeOp::Delete)]
#[case(WorktreeOp::Pull)]
#[case(WorktreeOp::Fetch)]
#[case(WorktreeOp::Status)]
#[tokio::test]
async fn worktree_op_no_manager(#[case] op: WorktreeOp) {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    let result = send_worktree_command(&cmd_tx, op, PathBuf::from("/any/path")).await;
    assert!(
        matches!(result, Err(SessionError::WorktreeNotConfigured)),
        "{op:?} should fail with WorktreeNotConfigured: {result:?}"
    );
}

#[tokio::test]
async fn create_session_success() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    // Spawn manager
    tokio::spawn(manager.run(cmd_rx));

    // Create session
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(result.is_ok());

    // Check for created event (may receive other events first)
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if matches!(event, SessionEvent::Created { .. }) {
                return true;
            }
        }
        false
    })
    .await;

    assert!(matches!(timeout, Ok(true)));
}

#[tokio::test]
async fn create_exceeds_limit() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager_with_limit(1, pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create first session (should succeed)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test1".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    assert!(response_rx.await.expect("response failed").is_ok());

    // Create second session (should fail)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test2".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(matches!(result, Err(SessionError::LimitReached { max: 1 })));
}

#[tokio::test]
async fn terminate_removes_session() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create session
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    let id = response_rx
        .await
        .expect("response failed")
        .expect("create failed");

    // Terminate session
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Terminate { id, response_tx })
        .await
        .expect("send failed");
    assert!(response_rx.await.expect("response failed").is_ok());

    // List sessions (should be empty)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::List { response_tx })
        .await
        .expect("send failed");
    let sessions = response_rx.await.expect("response failed");
    assert!(sessions.is_empty());
}

#[tokio::test]
async fn terminate_not_found() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Try to terminate non-existent session
    let fake_id = SessionId::new_v4();
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Terminate {
            id: fake_id,
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(matches!(result, Err(SessionError::NotFound { .. })));
}

#[tokio::test]
async fn input_forwarded() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let pty_clone = pty.clone();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create session
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    let id = response_rx
        .await
        .expect("response failed")
        .expect("create failed");

    // Send input
    cmd_tx
        .send(SessionCommand::SendInput {
            id,
            data: b"hello".to_vec(),
        })
        .await
        .expect("send failed");

    // Give time for input to be processed
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;

    // Verify input was captured
    assert_eq!(pty_clone.captured_input(), b"hello");
}

#[tokio::test]
async fn list_sessions() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create two sessions
    for name in ["session1", "session2"] {
        let (response_tx, response_rx) = oneshot::channel();
        cmd_tx
            .send(SessionCommand::Create {
                cmd: name.to_string(),
                args: vec![],
                cwd: None,
                branch_name: None,
                rows: 24,
                cols: 80,
                response_tx,
            })
            .await
            .expect("send failed");
        response_rx
            .await
            .expect("response failed")
            .expect("create failed");
    }

    // List sessions
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::List { response_tx })
        .await
        .expect("send failed");
    let sessions = response_rx.await.expect("response failed");
    assert_eq!(sessions.len(), 2);
}

#[tokio::test]
async fn resize_session() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let pty_clone = pty.clone();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create session
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    let id = response_rx
        .await
        .expect("response failed")
        .expect("create failed");

    // Resize session
    cmd_tx
        .send(SessionCommand::Resize {
            id,
            rows: 50,
            cols: 120,
        })
        .await
        .expect("send failed");

    // Give time for resize to be processed
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;

    assert_eq!(pty_clone.current_size(), (50, 120));
}

#[tokio::test]
async fn output_generates_event() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let pty_clone = pty.clone();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create session
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    let id = response_rx
        .await
        .expect("response failed")
        .expect("create failed");

    // Inject output
    pty_clone.inject_output(b"Hello, World!");

    // Wait for output event (skip other events)
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::Output { id: event_id, data } = event {
                assert_eq!(event_id, id);
                assert_eq!(data, b"Hello, World!");
                return true;
            }
        }
        false
    })
    .await;

    assert!(matches!(timeout, Ok(true)));
}

#[tokio::test]
async fn title_change_generates_event() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let pty_clone = pty.clone();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create session
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    let id = response_rx
        .await
        .expect("response failed")
        .expect("create failed");

    // Consume created event
    let _ = event_rx.recv().await;

    // Inject OSC title sequence
    pty_clone.inject_output(b"\x1b]0;New Title\x07");

    // Wait for title changed event
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        loop {
            if let Some(event @ SessionEvent::TitleChanged { .. }) = event_rx.recv().await {
                return event;
            }
        }
    })
    .await;

    if let Ok(event) = timeout {
        match event {
            SessionEvent::TitleChanged {
                id: event_id,
                title,
            } => {
                assert_eq!(event_id, id);
                assert_eq!(title, "New Title");
            }
            _ => panic!("Expected TitleChanged event"),
        }
    }
}

#[tokio::test]
async fn input_to_nonexistent_session() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Send input to non-existent session (should not panic)
    let fake_id = SessionId::new_v4();
    cmd_tx
        .send(SessionCommand::SendInput {
            id: fake_id,
            data: b"hello".to_vec(),
        })
        .await
        .expect("send failed");

    // Give time for processing
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}

#[tokio::test]
async fn resize_nonexistent_session() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Resize non-existent session (should not panic)
    let fake_id = SessionId::new_v4();
    cmd_tx
        .send(SessionCommand::Resize {
            id: fake_id,
            rows: 50,
            cols: 120,
        })
        .await
        .expect("send failed");

    // Give time for processing
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}

#[tokio::test]
async fn create_from_worktree_path_not_found() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let manager = SessionManager::new(
        10,
        pty,
        Some(wt),
        false,
        PullStrategy::default(),
        event_tx,
        MockGitHubClient::new(),
    );

    tokio::spawn(manager.run(cmd_rx));

    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CreateFromWorktree {
            worktree_path: PathBuf::from("/nonexistent/path"),
            cmd: "test".to_string(),
            args: vec![],
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(matches!(result, Err(SessionError::WorktreeNotFound { .. })));
}

#[tokio::test]
async fn create_from_worktree_success() {
    use std::fs;

    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    // Create temp directory for worktree
    let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
    let worktree_path = temp_dir.path().join("my-worktree");
    fs::create_dir_all(&worktree_path).expect("failed to create worktree dir");

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let manager = SessionManager::new(
        10,
        pty,
        Some(wt),
        false,
        PullStrategy::default(),
        event_tx,
        MockGitHubClient::new(),
    );

    tokio::spawn(manager.run(cmd_rx));

    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CreateFromWorktree {
            worktree_path: worktree_path.clone(),
            cmd: "test".to_string(),
            args: vec![],
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(result.is_ok());
    let returned_id = result.unwrap();

    // Check for created event
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::Created { id, .. } = event {
                return Some(id);
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some(id)) if id == returned_id));
}

#[tokio::test]
async fn create_from_worktree_exceeds_limit() {
    use std::fs;

    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    // Create temp directories for worktrees
    let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
    let worktree_path1 = temp_dir.path().join("worktree1");
    let worktree_path2 = temp_dir.path().join("worktree2");
    fs::create_dir_all(&worktree_path1).expect("failed to create worktree dir");
    fs::create_dir_all(&worktree_path2).expect("failed to create worktree dir");

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    // Limit to 1 session
    let manager = SessionManager::new(
        1,
        pty,
        Some(wt),
        false,
        PullStrategy::default(),
        event_tx,
        MockGitHubClient::new(),
    );

    tokio::spawn(manager.run(cmd_rx));

    // First creation should succeed
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CreateFromWorktree {
            worktree_path: worktree_path1,
            cmd: "test".to_string(),
            args: vec![],
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    assert!(response_rx.await.expect("response failed").is_ok());

    // Second creation should fail due to limit
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CreateFromWorktree {
            worktree_path: worktree_path2,
            cmd: "test".to_string(),
            args: vec![],
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(matches!(result, Err(SessionError::LimitReached { max: 1 })));
}

#[tokio::test]
async fn list_worktrees_with_manager() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    // Pre-create worktrees
    wt.create_worktree(Some("branch1"))
        .expect("create worktree");
    wt.create_worktree(Some("branch2"))
        .expect("create worktree");
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::ListWorktrees { response_tx })
        .await
        .expect("send failed");

    let worktrees = response_rx.await.expect("response failed");
    assert_eq!(worktrees.len(), 2);
}

#[tokio::test]
async fn list_worktrees_no_manager() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::ListWorktrees { response_tx })
        .await
        .expect("send failed");

    let worktrees = response_rx.await.expect("response failed");
    assert!(worktrees.is_empty());
}

#[tokio::test]
async fn terminate_with_auto_cleanup() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    // Enable auto_cleanup
    let manager = SessionManager::new(
        10,
        pty,
        Some(wt),
        true, // auto_cleanup = true
        PullStrategy::default(),
        event_tx,
        MockGitHubClient::new(),
    );

    tokio::spawn(manager.run(cmd_rx));

    // Create session (this will create a worktree)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: Some("test-branch".to_string()),
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    let id = response_rx
        .await
        .expect("response failed")
        .expect("create failed");

    // Terminate (should auto-cleanup worktree)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Terminate { id, response_tx })
        .await
        .expect("send failed");
    assert!(response_rx.await.expect("response failed").is_ok());

    // List sessions (should be empty after terminate)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::List { response_tx })
        .await
        .expect("send failed");
    let sessions = response_rx.await.expect("response failed");
    assert!(sessions.is_empty());
}

#[tokio::test]
async fn create_with_worktree_manager() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create session with branch name
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: Some("feature/test".to_string()),
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(result.is_ok());

    // Check for created event
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if matches!(event, SessionEvent::Created { .. }) {
                return true;
            }
        }
        false
    })
    .await;

    assert!(matches!(timeout, Ok(true)));
}

#[test]
fn session_count() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let pty = MockPty::new();
    let manager: TestManager = create_test_manager(pty, event_tx);
    assert_eq!(manager.session_count(), 0);
}

#[tokio::test]
async fn close_session_removes_session() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create session
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    let id = response_rx
        .await
        .expect("response failed")
        .expect("create failed");

    // Close session
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CloseSession { id, response_tx })
        .await
        .expect("send failed");
    let result = response_rx.await.expect("response failed");
    assert!(result.is_ok());

    // List sessions (should be empty)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::List { response_tx })
        .await
        .expect("send failed");
    let sessions = response_rx.await.expect("response failed");
    assert!(sessions.is_empty());
}

#[tokio::test]
async fn close_session_not_found() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Close non-existent session
    let fake_id = SessionId::new_v4();
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CloseSession {
            id: fake_id,
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(matches!(result, Err(SessionError::NotFound { .. })));
}

#[tokio::test]
async fn close_session_preserves_worktree() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let manager = SessionManager::new(
        10,
        pty,
        Some(wt),
        false,
        PullStrategy::default(),
        event_tx,
        MockGitHubClient::new(),
    );

    tokio::spawn(manager.run(cmd_rx));

    // Create session with worktree
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: Some("test-branch".to_string()),
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    let id = response_rx
        .await
        .expect("response failed")
        .expect("create failed");

    // Close session (worktree preserved)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CloseSession { id, response_tx })
        .await
        .expect("send failed");
    let result = response_rx.await.expect("response failed");

    // Should return Ok with Some(path) - worktree path returned but not cleaned up
    assert!(result.is_ok());
    let worktree_path = result.unwrap();
    assert!(worktree_path.is_some());
}

#[tokio::test]
async fn create_with_auto_input_success() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create session with auto-input
    let auto_input = vec![
        AutoInputStep {
            data: b"/plan\n".to_vec(),
            delay_ms: 100,
        },
        AutoInputStep {
            data: b"test prompt\n".to_vec(),
            delay_ms: 50,
        },
    ];

    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CreateWithAutoInput {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            auto_input: auto_input.clone(),
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(result.is_ok());
    let id = result.unwrap();

    // Check for created event with auto_input
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::Created {
                id: event_id,
                auto_input: event_auto_input,
                ..
            } = event
                && event_id == id
            {
                return Some(event_auto_input);
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some(steps)) if steps.len() == 2));
}

#[tokio::test]
async fn create_with_auto_input_exceeds_limit() {
    let (event_tx, _event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let manager = create_test_manager_with_limit(1, pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Create first session (should succeed)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::Create {
            cmd: "test1".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            response_tx,
        })
        .await
        .expect("send failed");
    assert!(response_rx.await.expect("response failed").is_ok());

    // Create second session with auto-input (should fail due to limit)
    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CreateWithAutoInput {
            cmd: "test2".to_string(),
            args: vec![],
            cwd: None,
            branch_name: None,
            rows: 24,
            cols: 80,
            auto_input: vec![AutoInputStep {
                data: b"test\n".to_vec(),
                delay_ms: 0,
            }],
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(matches!(result, Err(SessionError::LimitReached { max: 1 })));
}

#[tokio::test]
async fn create_with_auto_input_with_worktree() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    let auto_input = vec![AutoInputStep {
        data: b"hello\n".to_vec(),
        delay_ms: 0,
    }];

    let (response_tx, response_rx) = oneshot::channel();
    cmd_tx
        .send(SessionCommand::CreateWithAutoInput {
            cmd: "test".to_string(),
            args: vec![],
            cwd: None,
            branch_name: Some("feature/test".to_string()),
            rows: 24,
            cols: 80,
            auto_input,
            response_tx,
        })
        .await
        .expect("send failed");

    let result = response_rx.await.expect("response failed");
    assert!(result.is_ok());

    // Check for created event with branch
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::Created { branch, .. } = event {
                return branch;
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some(_))));
}

// ============================================
// GitHub Integration Tests
// ============================================

fn create_test_manager_with_github(
    pty: MockPty,
    github_client: MockGitHubClient,
    event_tx: mpsc::Sender<SessionEvent>,
) -> SessionManager<MockPty, MockWorktreeManager, MockGitHubClient> {
    SessionManager::new(
        10,
        pty,
        None,
        false,
        PullStrategy::default(),
        event_tx,
        github_client,
    )
}

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

    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let github_client = MockGitHubClient::new().with_issues(vec![GitHubIssue {
        number: 42,
        title: "Test Issue".to_string(),
        body: "Test body".to_string(),
        labels: vec![],
        state: IssueState::Open,
    }]);

    let manager = create_test_manager_with_github(pty, github_client, event_tx);
    tokio::spawn(manager.run(cmd_rx));

    // Send fetch issues command
    cmd_tx
        .send(SessionCommand::FetchIssuesAsync)
        .await
        .expect("send failed");

    // Wait for IssuesFetched event
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::IssuesFetched { result } = event {
                return Some(result);
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some(Ok(issues))) if issues.len() == 1));
}

#[tokio::test]
async fn fetch_issues_async_sends_error_event() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let github_client = MockGitHubClient::new().with_error("simulated error");

    let manager = create_test_manager_with_github(pty, github_client, event_tx);
    tokio::spawn(manager.run(cmd_rx));

    cmd_tx
        .send(SessionCommand::FetchIssuesAsync)
        .await
        .expect("send failed");

    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::IssuesFetched { result } = event {
                return Some(result);
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some(Err(_)))));
}

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

    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let github_client = MockGitHubClient::new()
        .with_issues(vec![GitHubIssue {
            number: 42,
            title: "Test Issue".to_string(),
            body: "Test body".to_string(),
            labels: vec![],
            state: IssueState::Open,
        }])
        .with_choices(vec![ActionChoice {
            branch: "feat/test".to_string(),
            action: ActionType::Implement,
            prompt: "Implement #42".to_string(),
        }]);

    let manager = create_test_manager_with_github(pty, github_client, event_tx);
    tokio::spawn(manager.run(cmd_rx));

    cmd_tx
        .send(SessionCommand::GenerateIssueActions { issue_number: 42 })
        .await
        .expect("send failed");

    // Collect events
    let mut issue_fetched = false;
    let mut actions_result = None;

    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            match event {
                SessionEvent::IssueFetched { issue_number } => {
                    assert_eq!(issue_number, 42);
                    issue_fetched = true;
                }
                SessionEvent::IssueActionsFetched {
                    issue_number,
                    result,
                } => {
                    assert_eq!(issue_number, 42);
                    actions_result = Some(result);
                    break;
                }
                _ => {}
            }
        }
    })
    .await;

    assert!(timeout.is_ok());
    assert!(issue_fetched);
    assert!(matches!(actions_result, Some(Ok(choices)) if choices.len() == 1));
}

#[tokio::test]
async fn generate_issue_actions_issue_not_found() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    // No issues configured - fetch_issue will return error
    let github_client = MockGitHubClient::new();

    let manager = create_test_manager_with_github(pty, github_client, event_tx);
    tokio::spawn(manager.run(cmd_rx));

    cmd_tx
        .send(SessionCommand::GenerateIssueActions { issue_number: 999 })
        .await
        .expect("send failed");

    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::IssueActionsFetched {
                issue_number,
                result,
            } = event
            {
                return Some((issue_number, result));
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some((999, Err(_))))));
}

// ============================================
// Async Worktree Operation Tests
// ============================================

#[tokio::test]
async fn refresh_worktrees_async_sends_event() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    // Pre-create worktrees
    wt.create_worktree(Some("branch1"))
        .expect("create worktree");
    wt.create_worktree(Some("branch2"))
        .expect("create worktree");
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Send refresh command
    cmd_tx
        .send(SessionCommand::RefreshWorktreesAsync)
        .await
        .expect("send failed");

    // Wait for final WorktreesRefreshed event (fetch_pending: false)
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        let mut last_worktrees = None;
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::WorktreesRefreshed {
                worktrees,
                fetch_pending,
            } = event
            {
                last_worktrees = Some(worktrees);
                if !fetch_pending {
                    break;
                }
            }
        }
        last_worktrees
    })
    .await;

    assert!(matches!(timeout, Ok(Some(worktrees)) if worktrees.len() == 2));
}

#[tokio::test]
async fn refresh_worktrees_async_no_manager() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    // No worktree manager
    let manager = create_test_manager(pty, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    cmd_tx
        .send(SessionCommand::RefreshWorktreesAsync)
        .await
        .expect("send failed");

    // Should receive empty worktrees (no fetch_pending since no manager)
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::WorktreesRefreshed { worktrees, .. } = event {
                return Some(worktrees);
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some(worktrees)) if worktrees.is_empty()));
}

#[tokio::test]
async fn delete_worktree_async_sends_event() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let (worktree_path, _branch) = wt
        .create_worktree(Some("test-branch"))
        .expect("create worktree");
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    cmd_tx
        .send(SessionCommand::DeleteWorktreeAsync {
            path: worktree_path.clone(),
        })
        .await
        .expect("send failed");

    // Wait for WorktreeDeleted event
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::WorktreeDeleted { path, result } = event {
                return Some((path, result));
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some((path, Ok(())))) if path == worktree_path));
}

#[tokio::test]
async fn delete_worktree_async_error() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Delete non-existent worktree
    cmd_tx
        .send(SessionCommand::DeleteWorktreeAsync {
            path: PathBuf::from("/nonexistent/path"),
        })
        .await
        .expect("send failed");

    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::WorktreeDeleted { path, result } = event {
                return Some((path, result));
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some((_, Err(_))))));
}

#[tokio::test]
async fn pull_worktree_async_sends_event() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let (worktree_path, _branch) = wt
        .create_worktree(Some("test-branch"))
        .expect("create worktree");
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    cmd_tx
        .send(SessionCommand::PullWorktreeAsync {
            path: worktree_path.clone(),
        })
        .await
        .expect("send failed");

    // Wait for WorktreePulled event
    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::WorktreePulled { path, result } = event {
                return Some((path, result));
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some((path, Ok(_)))) if path == worktree_path));
}

#[tokio::test]
async fn pull_worktree_async_error() {
    let (event_tx, mut event_rx) = mpsc::channel(16);
    let (cmd_tx, cmd_rx) = mpsc::channel(16);

    let pty = MockPty::new();
    let wt = MockWorktreeManager::new(PathBuf::from("/repo"), true);
    let manager = create_test_manager_with_worktree(pty, wt, event_tx);

    tokio::spawn(manager.run(cmd_rx));

    // Pull non-existent worktree
    cmd_tx
        .send(SessionCommand::PullWorktreeAsync {
            path: PathBuf::from("/nonexistent/path"),
        })
        .await
        .expect("send failed");

    let timeout = tokio::time::timeout(tokio::time::Duration::from_millis(500), async {
        while let Some(event) = event_rx.recv().await {
            if let SessionEvent::WorktreePulled { path, result } = event {
                return Some((path, result));
            }
        }
        None
    })
    .await;

    assert!(matches!(timeout, Ok(Some((_, Err(_))))));
}
