use super::*;
use codex_app_server_protocol::FileSystemAccessMode;
use codex_app_server_protocol::FileSystemPath;
use codex_app_server_protocol::FileSystemSandboxEntry;
use codex_app_server_protocol::FileSystemSpecialPath;
use codex_app_server_protocol::NetworkAccess;
use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile;
use codex_app_server_protocol::PermissionProfileFileSystemPermissions;
use codex_app_server_protocol::PermissionProfileNetworkPermissions;
use codex_app_server_protocol::SandboxPolicy;
use pretty_assertions::assert_eq;

#[tokio::test]
async fn resumed_initial_messages_render_history() {
    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;

    let thread_id = ThreadId::new();
    let rollout_file = NamedTempFile::new().unwrap();
    let configured = crate::session_state::ThreadSessionState {
        thread_id,
        forked_from_id: None,
        fork_parent_title: None,
        thread_name: None,
        model: "test-model".to_string(),
        model_provider_id: "test-provider".to_string(),
        service_tier: None,
        approval_policy: AskForApproval::Never,
        approvals_reviewer: ApprovalsReviewer::User,
        permission_profile: PermissionProfile::read_only(),
        active_permission_profile: None,
        cwd: test_path_buf("/home/user/project").abs(),
        instruction_source_paths: Vec::new(),
        reasoning_effort: Some(ReasoningEffortConfig::default()),
        message_history: None,
        network_proxy: None,
        rollout_path: Some(rollout_file.path().to_path_buf()),
    };

    chat.handle_thread_session(configured);
    replay_user_message_text(
        &mut chat,
        "user-1",
        "hello from user",
        ReplayKind::ResumeInitialMessages,
    );
    replay_agent_message(
        &mut chat,
        "assistant-1",
        "assistant reply",
        ReplayKind::ResumeInitialMessages,
    );

    let cells = drain_insert_history(&mut rx);
    let mut merged_lines = Vec::new();
    for lines in cells {
        let text = lines
            .iter()
            .flat_map(|line| line.spans.iter())
            .map(|span| span.content.clone())
            .collect::<String>();
        merged_lines.push(text);
    }

    let text_blob = merged_lines.join("\n");
    assert!(
        text_blob.contains("hello from user"),
        "expected replayed user message",
    );
    assert!(
        text_blob.contains("assistant reply"),
        "expected replayed agent message",
    );
}

#[tokio::test]
async fn replayed_user_message_preserves_text_elements_and_local_images() {
    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;

    let placeholder = "[Image #1]";
    let message = format!("{placeholder} replayed");
    let text_elements = vec![TextElement::new(
        (0..placeholder.len()).into(),
        Some(placeholder.to_string()),
    )];
    let local_images = vec![PathBuf::from("/tmp/replay.png")];

    let thread_id = ThreadId::new();
    let rollout_file = NamedTempFile::new().unwrap();
    let configured = crate::session_state::ThreadSessionState {
        thread_id,
        forked_from_id: None,
        fork_parent_title: None,
        thread_name: None,
        model: "test-model".to_string(),
        model_provider_id: "test-provider".to_string(),
        service_tier: None,
        approval_policy: AskForApproval::Never,
        approvals_reviewer: ApprovalsReviewer::User,
        permission_profile: PermissionProfile::read_only(),
        active_permission_profile: None,
        cwd: test_path_buf("/home/user/project").abs(),
        instruction_source_paths: Vec::new(),
        reasoning_effort: Some(ReasoningEffortConfig::default()),
        message_history: None,
        network_proxy: None,
        rollout_path: Some(rollout_file.path().to_path_buf()),
    };

    chat.handle_thread_session(configured);
    replay_user_message_inputs(
        &mut chat,
        "user-1",
        vec![
            AppServerUserInput::Text {
                text: message.clone(),
                text_elements: text_elements.clone().into_iter().map(Into::into).collect(),
            },
            AppServerUserInput::LocalImage {
                path: local_images[0].clone(),
            },
        ],
        ReplayKind::ResumeInitialMessages,
    );

    let mut user_cell = None;
    while let Ok(ev) = rx.try_recv() {
        if let AppEvent::InsertHistoryCell(cell) = ev
            && let Some(cell) = cell.as_any().downcast_ref::<UserHistoryCell>()
        {
            user_cell = Some((
                cell.message.clone(),
                cell.text_elements.clone(),
                cell.local_image_paths.clone(),
                cell.remote_image_urls.clone(),
            ));
            break;
        }
    }

    let (stored_message, stored_elements, stored_images, stored_remote_image_urls) =
        user_cell.expect("expected a replayed user history cell");
    assert_eq!(stored_message, message);
    assert_eq!(stored_elements, text_elements);
    assert_eq!(stored_images, local_images);
    assert!(stored_remote_image_urls.is_empty());
}

#[tokio::test]
async fn replayed_user_message_preserves_remote_image_urls() {
    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;

    let message = "replayed with remote image".to_string();
    let remote_image_urls = vec!["https://example.com/image.png".to_string()];

    let thread_id = ThreadId::new();
    let rollout_file = NamedTempFile::new().unwrap();
    let configured = crate::session_state::ThreadSessionState {
        thread_id,
        forked_from_id: None,
        fork_parent_title: None,
        thread_name: None,
        model: "test-model".to_string(),
        model_provider_id: "test-provider".to_string(),
        service_tier: None,
        approval_policy: AskForApproval::Never,
        approvals_reviewer: ApprovalsReviewer::User,
        permission_profile: PermissionProfile::read_only(),
        active_permission_profile: None,
        cwd: test_path_buf("/home/user/project").abs(),
        instruction_source_paths: Vec::new(),
        reasoning_effort: Some(ReasoningEffortConfig::default()),
        message_history: None,
        network_proxy: None,
        rollout_path: Some(rollout_file.path().to_path_buf()),
    };

    chat.handle_thread_session(configured);
    replay_user_message_inputs(
        &mut chat,
        "user-1",
        vec![
            AppServerUserInput::Text {
                text: message.clone(),
                text_elements: Vec::new(),
            },
            AppServerUserInput::Image {
                url: remote_image_urls[0].clone(),
            },
        ],
        ReplayKind::ResumeInitialMessages,
    );

    let mut user_cell = None;
    while let Ok(ev) = rx.try_recv() {
        if let AppEvent::InsertHistoryCell(cell) = ev
            && let Some(cell) = cell.as_any().downcast_ref::<UserHistoryCell>()
        {
            user_cell = Some((
                cell.message.clone(),
                cell.local_image_paths.clone(),
                cell.remote_image_urls.clone(),
            ));
            break;
        }
    }

    let (stored_message, stored_local_images, stored_remote_image_urls) =
        user_cell.expect("expected a replayed user history cell");
    assert_eq!(stored_message, message);
    assert!(stored_local_images.is_empty());
    assert_eq!(stored_remote_image_urls, remote_image_urls);
}

#[tokio::test]
async fn session_configured_syncs_widget_config_permissions_and_cwd() {
    let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;

    chat.config
        .permissions
        .approval_policy
        .set(AskForApproval::OnRequest.to_core())
        .expect("set approval policy");
    chat.config
        .permissions
        .set_permission_profile(PermissionProfile::workspace_write())
        .expect("set permission profile");
    chat.config.cwd = test_path_buf("/home/user/main").abs();

    let expected_cwd = test_path_buf("/home/user/sub-agent").abs();
    let expected_app_server_permission_profile = AppServerPermissionProfile::Managed {
        network: PermissionProfileNetworkPermissions { enabled: false },
        file_system: PermissionProfileFileSystemPermissions::Restricted {
            entries: vec![
                FileSystemSandboxEntry {
                    path: FileSystemPath::Special {
                        value: FileSystemSpecialPath::Root,
                    },
                    access: FileSystemAccessMode::Read,
                },
                FileSystemSandboxEntry {
                    path: FileSystemPath::GlobPattern {
                        pattern: "**/.secret".to_string(),
                    },
                    access: FileSystemAccessMode::None,
                },
            ],
            glob_scan_max_depth: None,
        },
    };
    let expected_permission_profile: PermissionProfile =
        expected_app_server_permission_profile.clone().into();
    let expected_core_sandbox = expected_permission_profile
        .to_legacy_sandbox_policy(expected_cwd.as_path())
        .expect("permission profile should project to legacy sandbox policy");
    let expected_sandbox = SandboxPolicy::from(expected_core_sandbox);
    let configured = crate::session_state::ThreadSessionState {
        thread_id: ThreadId::new(),
        forked_from_id: None,
        fork_parent_title: None,
        thread_name: None,
        model: "test-model".to_string(),
        model_provider_id: "test-provider".to_string(),
        service_tier: None,
        approval_policy: AskForApproval::Never,
        approvals_reviewer: ApprovalsReviewer::User,
        permission_profile: expected_permission_profile,
        active_permission_profile: None,
        cwd: expected_cwd.clone(),
        instruction_source_paths: Vec::new(),
        reasoning_effort: Some(ReasoningEffortConfig::default()),
        message_history: None,
        network_proxy: None,
        rollout_path: None,
    };

    chat.handle_thread_session(configured);

    assert_eq!(
        AskForApproval::from(chat.config_ref().permissions.approval_policy.value()),
        AskForApproval::Never
    );
    let actual_sandbox = SandboxPolicy::from(chat.config_ref().legacy_sandbox_policy());
    assert_eq!(&actual_sandbox, &expected_sandbox);
    assert_eq!(
        AppServerPermissionProfile::from(chat.config_ref().permissions.permission_profile()),
        expected_app_server_permission_profile
    );
    assert_eq!(&chat.config_ref().cwd, &expected_cwd);

    let updated_profile = PermissionProfile::workspace_write();
    chat.set_permission_profile(updated_profile.clone())
        .expect("set permission profile");
    assert_eq!(
        chat.config_ref().permissions.permission_profile(),
        updated_profile,
        "local permission changes should replace SessionConfigured profile-derived runtime permissions"
    );
}

#[tokio::test]
async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
    let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;

    let expected_app_server_permission_profile = AppServerPermissionProfile::External {
        network: PermissionProfileNetworkPermissions { enabled: false },
    };
    let expected_permission_profile: PermissionProfile =
        expected_app_server_permission_profile.clone().into();
    let expected_sandbox = SandboxPolicy::ExternalSandbox {
        network_access: NetworkAccess::Restricted,
    };
    let configured = crate::session_state::ThreadSessionState {
        thread_id: ThreadId::new(),
        forked_from_id: None,
        fork_parent_title: None,
        thread_name: None,
        model: "test-model".to_string(),
        model_provider_id: "test-provider".to_string(),
        service_tier: None,
        approval_policy: AskForApproval::Never,
        approvals_reviewer: ApprovalsReviewer::User,
        permission_profile: expected_permission_profile,
        active_permission_profile: None,
        cwd: test_path_buf("/home/user/external").abs(),
        instruction_source_paths: Vec::new(),
        reasoning_effort: Some(ReasoningEffortConfig::default()),
        message_history: None,
        network_proxy: None,
        rollout_path: None,
    };

    chat.handle_thread_session(configured);

    let actual_sandbox = SandboxPolicy::from(chat.config_ref().legacy_sandbox_policy());
    assert_eq!(&actual_sandbox, &expected_sandbox);
    assert_eq!(
        AppServerPermissionProfile::from(chat.config_ref().permissions.permission_profile()),
        expected_app_server_permission_profile
    );
}

#[tokio::test]
async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;

    let remote_image_urls = vec!["https://example.com/remote-only.png".to_string()];

    let thread_id = ThreadId::new();
    let rollout_file = NamedTempFile::new().unwrap();
    let configured = crate::session_state::ThreadSessionState {
        thread_id,
        forked_from_id: None,
        fork_parent_title: None,
        thread_name: None,
        model: "test-model".to_string(),
        model_provider_id: "test-provider".to_string(),
        service_tier: None,
        approval_policy: AskForApproval::Never,
        approvals_reviewer: ApprovalsReviewer::User,
        permission_profile: PermissionProfile::read_only(),
        active_permission_profile: None,
        cwd: test_path_buf("/home/user/project").abs(),
        instruction_source_paths: Vec::new(),
        reasoning_effort: Some(ReasoningEffortConfig::default()),
        message_history: None,
        network_proxy: None,
        rollout_path: Some(rollout_file.path().to_path_buf()),
    };

    chat.handle_thread_session(configured);
    replay_user_message_inputs(
        &mut chat,
        "user-1",
        vec![AppServerUserInput::Image {
            url: remote_image_urls[0].clone(),
        }],
        ReplayKind::ResumeInitialMessages,
    );

    let mut user_cell = None;
    while let Ok(ev) = rx.try_recv() {
        if let AppEvent::InsertHistoryCell(cell) = ev
            && let Some(cell) = cell.as_any().downcast_ref::<UserHistoryCell>()
        {
            user_cell = Some((cell.message.clone(), cell.remote_image_urls.clone()));
            break;
        }
    }

    let (stored_message, stored_remote_image_urls) =
        user_cell.expect("expected a replayed remote-image-only user history cell");
    assert!(stored_message.is_empty());
    assert_eq!(stored_remote_image_urls, remote_image_urls);
}

#[tokio::test]
async fn replayed_user_message_with_only_local_images_renders_history_cell() {
    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;

    let local_images = [PathBuf::from("/tmp/replay-local-only.png")];

    let thread_id = ThreadId::new();
    let rollout_file = NamedTempFile::new().unwrap();
    let configured = crate::session_state::ThreadSessionState {
        thread_id,
        forked_from_id: None,
        fork_parent_title: None,
        thread_name: None,
        model: "test-model".to_string(),
        model_provider_id: "test-provider".to_string(),
        service_tier: None,
        approval_policy: AskForApproval::Never,
        approvals_reviewer: ApprovalsReviewer::User,
        permission_profile: PermissionProfile::read_only(),
        active_permission_profile: None,
        cwd: test_path_buf("/home/user/project").abs(),
        instruction_source_paths: Vec::new(),
        reasoning_effort: Some(ReasoningEffortConfig::default()),
        message_history: None,
        network_proxy: None,
        rollout_path: Some(rollout_file.path().to_path_buf()),
    };

    chat.handle_thread_session(configured);
    replay_user_message_inputs(
        &mut chat,
        "user-1",
        vec![AppServerUserInput::LocalImage {
            path: local_images[0].clone(),
        }],
        ReplayKind::ResumeInitialMessages,
    );

    let mut user_cell = None;
    while let Ok(ev) = rx.try_recv() {
        if let AppEvent::InsertHistoryCell(cell) = ev
            && let Some(cell) = cell.as_any().downcast_ref::<UserHistoryCell>()
        {
            user_cell = Some((cell.message.clone(), cell.local_image_paths.clone()));
            break;
        }
    }

    let (stored_message, stored_local_images) =
        user_cell.expect("expected a replayed local-image-only user history cell");
    assert!(stored_message.is_empty());
    assert_eq!(stored_local_images, local_images);
}

#[tokio::test]
async fn forked_thread_history_line_includes_name_and_id_snapshot() {
    let (chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
    let mut chat = chat;

    let forked_from_id =
        ThreadId::from_string("e9f18a88-8081-4e51-9d4e-8af5cde2d8dd").expect("forked id");

    chat.emit_forked_thread_event(forked_from_id, Some("named-thread".to_string()));

    let history_cell = tokio::time::timeout(std::time::Duration::from_secs(2), async {
        loop {
            match rx.recv().await {
                Some(AppEvent::InsertHistoryCell(cell)) => break cell,
                Some(_) => continue,
                None => panic!("app event channel closed before forked thread history was emitted"),
            }
        }
    })
    .await
    .expect("timed out waiting for forked thread history");
    let combined = lines_to_single_string(&history_cell.display_lines(/*width*/ 80));

    assert!(
        combined.contains("Thread forked from"),
        "expected forked thread message in history"
    );
    assert_chatwidget_snapshot!("forked_thread_history_line", combined);
}

#[tokio::test]
async fn forked_thread_history_line_without_name_shows_id_once_snapshot() {
    let (chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
    let mut chat = chat;
    let temp = tempdir().expect("tempdir");
    chat.config.codex_home =
        codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(temp.path())
            .expect("temp dir is absolute");

    let forked_from_id =
        ThreadId::from_string("019c2d47-4935-7423-a190-05691f566092").expect("forked id");
    chat.emit_forked_thread_event(forked_from_id, /*fork_parent_title*/ None);

    let history_cell = tokio::time::timeout(std::time::Duration::from_secs(2), async {
        loop {
            match rx.recv().await {
                Some(AppEvent::InsertHistoryCell(cell)) => break cell,
                Some(_) => continue,
                None => panic!("app event channel closed before forked thread history was emitted"),
            }
        }
    })
    .await
    .expect("timed out waiting for forked thread history");
    let combined = lines_to_single_string(&history_cell.display_lines(/*width*/ 80));

    assert_chatwidget_snapshot!("forked_thread_history_line_without_name", combined);
}

#[tokio::test]
async fn app_server_forked_thread_history_line_uses_app_server_title_snapshot() {
    let (chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
    let mut chat = chat;
    let temp = tempdir().expect("tempdir");
    chat.config.codex_home =
        codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(temp.path())
            .expect("temp dir is absolute");

    let forked_from_id =
        ThreadId::from_string("e9f18a88-8081-4e51-9d4e-8af5cde2d8dd").expect("forked id");
    let session_index_entry = format!(
        "{{\"id\":\"{forked_from_id}\",\"thread_name\":\"stale-local-thread\",\"updated_at\":\"2024-01-02T00:00:00Z\"}}\n"
    );
    std::fs::write(temp.path().join("session_index.jsonl"), session_index_entry)
        .expect("write session index");

    chat.emit_forked_thread_event(forked_from_id, Some("app-server-parent-thread".to_string()));

    let history_cell = tokio::time::timeout(std::time::Duration::from_secs(2), async {
        loop {
            match rx.recv().await {
                Some(AppEvent::InsertHistoryCell(cell)) => break cell,
                Some(_) => continue,
                None => panic!("app event channel closed before forked thread history was emitted"),
            }
        }
    })
    .await
    .expect("timed out waiting for forked thread history");
    let combined = lines_to_single_string(&history_cell.display_lines(/*width*/ 80));

    assert!(combined.contains("app-server-parent-thread"));
    assert!(
        !combined.contains("stale-local-thread"),
        "app-server fork title lookup should not read local CODEX_HOME"
    );
    assert_chatwidget_snapshot!("app_server_forked_thread_history_line", combined);
}

#[tokio::test]
async fn app_server_forked_thread_history_line_without_app_server_name_ignores_local_snapshot() {
    let (chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
    let mut chat = chat;
    let temp = tempdir().expect("tempdir");
    chat.config.codex_home =
        codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(temp.path())
            .expect("temp dir is absolute");

    let forked_from_id =
        ThreadId::from_string("019c2d47-4935-7423-a190-05691f566092").expect("forked id");
    let session_index_entry = format!(
        "{{\"id\":\"{forked_from_id}\",\"thread_name\":\"stale-local-thread\",\"updated_at\":\"2024-01-02T00:00:00Z\"}}\n"
    );
    std::fs::write(temp.path().join("session_index.jsonl"), session_index_entry)
        .expect("write session index");

    chat.emit_forked_thread_event(forked_from_id, /*fork_parent_title*/ None);

    let history_cell = tokio::time::timeout(std::time::Duration::from_secs(2), async {
        loop {
            match rx.recv().await {
                Some(AppEvent::InsertHistoryCell(cell)) => break cell,
                Some(_) => continue,
                None => panic!("app event channel closed before forked thread history was emitted"),
            }
        }
    })
    .await
    .expect("timed out waiting for forked thread history");
    let combined = lines_to_single_string(&history_cell.display_lines(/*width*/ 80));

    assert!(
        !combined.contains("stale-local-thread"),
        "app-server fork title lookup should not read local CODEX_HOME"
    );
    assert_chatwidget_snapshot!(
        "app_server_forked_thread_history_line_without_app_server_name",
        combined
    );
}

#[tokio::test]
async fn thread_snapshot_replay_preserves_agent_message_during_review_mode() {
    let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;

    replay_entered_review_mode(&mut chat, "current changes");
    let _ = drain_insert_history(&mut rx);

    replay_agent_message(
        &mut chat,
        "review-message",
        "Review progress update",
        ReplayKind::ThreadSnapshot,
    );

    let inserted = drain_insert_history(&mut rx);
    assert_eq!(inserted.len(), 1);
    assert!(lines_to_single_string(&inserted[0]).contains("Review progress update"));
}

#[tokio::test]
async fn replayed_retryable_app_server_error_keeps_turn_running() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

    chat.handle_server_notification(
        ServerNotification::TurnStarted(TurnStartedNotification {
            thread_id: "thread-1".to_string(),
            turn: AppServerTurn {
                id: "turn-1".to_string(),
                items_view: codex_app_server_protocol::TurnItemsView::Full,
                items: Vec::new(),
                status: AppServerTurnStatus::InProgress,
                error: None,
                started_at: Some(0),
                completed_at: None,
                duration_ms: None,
            },
        }),
        Some(ReplayKind::ThreadSnapshot),
    );
    drain_insert_history(&mut rx);

    chat.handle_server_notification(
        ServerNotification::Error(ErrorNotification {
            error: AppServerTurnError {
                message: "Reconnecting... 1/5".to_string(),
                codex_error_info: None,
                additional_details: Some("Idle timeout waiting for SSE".to_string()),
            },
            will_retry: true,
            thread_id: "thread-1".to_string(),
            turn_id: "turn-1".to_string(),
        }),
        Some(ReplayKind::ThreadSnapshot),
    );

    assert!(drain_insert_history(&mut rx).is_empty());
    assert!(chat.bottom_pane.is_task_running());
    let status = chat
        .bottom_pane
        .status_widget()
        .expect("status indicator should be visible");
    assert_eq!(status.header(), "Working");
    assert_eq!(status.details(), None);
}

#[tokio::test]
async fn replayed_thread_closed_notification_does_not_exit_tui() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

    chat.handle_server_notification(
        ServerNotification::ThreadClosed(ThreadClosedNotification {
            thread_id: "thread-1".to_string(),
        }),
        Some(ReplayKind::ThreadSnapshot),
    );

    assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
}

#[tokio::test]
async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
    chat.config.show_raw_agent_reasoning = false;
    chat.handle_thread_session(crate::session_state::ThreadSessionState {
        thread_id: ThreadId::new(),
        forked_from_id: None,
        fork_parent_title: None,
        thread_name: None,
        model: "test-model".to_string(),
        model_provider_id: "test-provider".to_string(),
        service_tier: None,
        approval_policy: AskForApproval::Never,
        approvals_reviewer: ApprovalsReviewer::User,
        permission_profile: PermissionProfile::read_only(),
        active_permission_profile: None,
        cwd: test_project_path().abs(),
        instruction_source_paths: Vec::new(),
        reasoning_effort: None,
        message_history: None,
        network_proxy: None,
        rollout_path: None,
    });
    let _ = drain_insert_history(&mut rx);

    chat.replay_thread_item(
        AppServerThreadItem::Reasoning {
            id: "reasoning-1".to_string(),
            summary: vec!["Summary only".to_string()],
            content: vec!["Raw reasoning".to_string()],
        },
        "turn-1".to_string(),
        ReplayKind::ThreadSnapshot,
    );

    let rendered = match rx.try_recv() {
        Ok(AppEvent::InsertHistoryCell(cell)) => {
            lines_to_single_string(&cell.transcript_lines(/*width*/ 80))
        }
        other => panic!("expected InsertHistoryCell, got {other:?}"),
    };
    assert!(!rendered.trim().is_empty());
    assert!(!rendered.contains("Raw reasoning"));
}

#[tokio::test]
async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
    chat.config.show_raw_agent_reasoning = true;
    chat.handle_thread_session(crate::session_state::ThreadSessionState {
        thread_id: ThreadId::new(),
        forked_from_id: None,
        fork_parent_title: None,
        thread_name: None,
        model: "test-model".to_string(),
        model_provider_id: "test-provider".to_string(),
        service_tier: None,
        approval_policy: AskForApproval::Never,
        approvals_reviewer: ApprovalsReviewer::User,
        permission_profile: PermissionProfile::read_only(),
        active_permission_profile: None,
        cwd: test_project_path().abs(),
        instruction_source_paths: Vec::new(),
        reasoning_effort: None,
        message_history: None,
        network_proxy: None,
        rollout_path: None,
    });
    let _ = drain_insert_history(&mut rx);

    chat.replay_thread_item(
        AppServerThreadItem::Reasoning {
            id: "reasoning-1".to_string(),
            summary: vec!["Summary only".to_string()],
            content: vec!["Raw reasoning".to_string()],
        },
        "turn-1".to_string(),
        ReplayKind::ThreadSnapshot,
    );

    let rendered = match rx.try_recv() {
        Ok(AppEvent::InsertHistoryCell(cell)) => {
            lines_to_single_string(&cell.transcript_lines(/*width*/ 80))
        }
        other => panic!("expected InsertHistoryCell, got {other:?}"),
    };
    assert!(rendered.contains("Raw reasoning"));
}

#[tokio::test]
async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
    chat.show_welcome_banner = false;

    chat.handle_server_notification(
        ServerNotification::TurnStarted(TurnStartedNotification {
            thread_id: "thread-1".to_string(),
            turn: AppServerTurn {
                id: "turn-1".to_string(),
                items_view: codex_app_server_protocol::TurnItemsView::Full,
                items: Vec::new(),
                status: AppServerTurnStatus::InProgress,
                error: None,
                started_at: Some(0),
                completed_at: None,
                duration_ms: None,
            },
        }),
        /*replay_kind*/ None,
    );
    let _ = drain_insert_history(&mut rx);

    chat.handle_server_notification(
        ServerNotification::ReasoningSummaryTextDelta(ReasoningSummaryTextDeltaNotification {
            thread_id: "thread-1".to_string(),
            turn_id: "turn-1".to_string(),
            item_id: "reasoning-1".to_string(),
            delta: "Summary only".to_string(),
            summary_index: 0,
        }),
        /*replay_kind*/ None,
    );

    chat.handle_server_notification(
        ServerNotification::ItemCompleted(ItemCompletedNotification {
            thread_id: "thread-1".to_string(),
            turn_id: "turn-1".to_string(),
            completed_at_ms: 0,
            item: AppServerThreadItem::Reasoning {
                id: "reasoning-1".to_string(),
                summary: vec!["Summary only".to_string()],
                content: Vec::new(),
            },
        }),
        /*replay_kind*/ None,
    );

    let rendered = match rx.try_recv() {
        Ok(AppEvent::InsertHistoryCell(cell)) => {
            lines_to_single_string(&cell.transcript_lines(/*width*/ 80))
        }
        other => panic!("expected InsertHistoryCell, got {other:?}"),
    };
    assert_eq!(rendered.matches("Summary only").count(), 1);
}

#[tokio::test]
async fn thread_snapshot_replayed_turn_started_marks_task_running() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

    replay_turn_started(&mut chat, ReplayKind::ThreadSnapshot);

    drain_insert_history(&mut rx);
    assert!(chat.bottom_pane.is_task_running());
    let status = chat
        .bottom_pane
        .status_widget()
        .expect("status indicator should be visible");
    assert_eq!(status.header(), "Working");
}

#[tokio::test]
async fn replayed_in_progress_turn_marks_task_running() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

    chat.replay_thread_turns(
        vec![AppServerTurn {
            id: "turn-1".to_string(),
            items_view: codex_app_server_protocol::TurnItemsView::Full,
            items: Vec::new(),
            status: AppServerTurnStatus::InProgress,
            error: None,
            started_at: None,
            completed_at: None,
            duration_ms: None,
        }],
        ReplayKind::ResumeInitialMessages,
    );

    assert!(drain_insert_history(&mut rx).is_empty());
    assert!(chat.bottom_pane.is_task_running());
    let status = chat
        .bottom_pane
        .status_widget()
        .expect("status indicator should be visible");
    assert_eq!(status.header(), "Working");
}

#[tokio::test]
async fn replayed_stream_error_does_not_set_retry_status_or_status_indicator() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
    chat.set_status_header("Idle".to_string());

    handle_stream_error_with_replay(
        &mut chat,
        "Reconnecting... 2/5",
        Some("Idle timeout waiting for SSE".to_string()),
        Some(ReplayKind::ResumeInitialMessages),
    );

    let cells = drain_insert_history(&mut rx);
    assert!(
        cells.is_empty(),
        "expected no history cell for replayed StreamError event"
    );
    assert_eq!(chat.status_state.current_status.header, "Idle");
    assert!(chat.status_state.retry_status_header.is_none());
    assert!(chat.bottom_pane.status_widget().is_none());
}

#[tokio::test]
async fn thread_snapshot_replayed_stream_recovery_restores_previous_status_header() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

    replay_turn_started(&mut chat, ReplayKind::ThreadSnapshot);
    drain_insert_history(&mut rx);

    handle_stream_error_with_replay(
        &mut chat,
        "Reconnecting... 1/5",
        /*additional_details*/ None,
        Some(ReplayKind::ThreadSnapshot),
    );
    drain_insert_history(&mut rx);

    replay_agent_message_delta(&mut chat, "hello", ReplayKind::ThreadSnapshot);

    let status = chat
        .bottom_pane
        .status_widget()
        .expect("status indicator should be visible");
    assert_eq!(status.header(), "Working");
    assert_eq!(status.details(), None);
    assert!(chat.status_state.retry_status_header.is_none());
}

#[tokio::test]
async fn stream_recovery_restores_previous_status_header() {
    let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
    handle_turn_started(&mut chat, "turn-1");
    drain_insert_history(&mut rx);
    handle_stream_error(
        &mut chat,
        "Reconnecting... 1/5",
        /*additional_details*/ None,
    );
    drain_insert_history(&mut rx);
    handle_agent_message_delta(&mut chat, "hello");

    let status = chat
        .bottom_pane
        .status_widget()
        .expect("status indicator should be visible");
    assert_eq!(status.header(), "Working");
    assert_eq!(status.details(), None);
    assert!(chat.status_state.retry_status_header.is_none());
}
