#![allow(clippy::unwrap_used)]

use std::fs;
use std::path::Path;

use codex_core::shell::default_user_shell;
use codex_features::Feature;
use codex_prompts::APPLY_PATCH_TOOL_INSTRUCTIONS;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
use core_test_support::TempDirExt;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::local_selections;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use tempfile::TempDir;

fn write_global_instructions(home: &Path) {
    fs::write(home.join("AGENTS.md"), "be consistent and helpful")
        .unwrap_or_else(|err| panic!("write global instructions: {err}"));
}

fn text_user_input(text: String) -> serde_json::Value {
    text_user_input_parts(vec![text])
}

fn text_user_input_parts(texts: Vec<String>) -> serde_json::Value {
    serde_json::json!({
        "type": "message",
        "role": "user",
        "content": texts
            .into_iter()
            .map(|text| serde_json::json!({ "type": "input_text", "text": text }))
            .collect::<Vec<_>>()
    })
}

fn assert_default_env_context(text: &str, cwd: &str) {
    assert_env_context_fragment(text);
    assert!(
        text.contains(&format!("<cwd>{cwd}</cwd>")),
        "expected cwd in environment context: {text}"
    );
    assert!(
        text.contains(&format!("<shell>{}</shell>", default_user_shell().name())),
        "expected shell in environment context: {text}"
    );
}

fn assert_env_context_fragment(text: &str) {
    assert!(
        text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG),
        "expected environment context fragment: {text}"
    );
    assert!(
        text.contains("<current_date>") && text.contains("</current_date>"),
        "expected current_date in environment context: {text}"
    );
    assert!(
        text.contains("<timezone>") && text.contains("</timezone>"),
        "expected timezone in environment context: {text}"
    );
    assert!(
        text.ends_with("</environment_context>"),
        "expected closing environment_context tag: {text}"
    );
}

fn assert_tool_names(body: &serde_json::Value, expected_names: &[&str]) {
    assert_eq!(
        body["tools"]
            .as_array()
            .unwrap()
            .iter()
            .map(|t| {
                t.get("name")
                    .and_then(|value| value.as_str())
                    .or_else(|| t.get("type").and_then(|value| value.as_str()))
                    .unwrap()
                    .to_string()
            })
            .collect::<Vec<_>>(),
        expected_names
    );
}

fn normalize_newlines(text: &str) -> String {
    text.replace("\r\n", "\n")
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
    skip_if_no_network!(Ok(()));
    use pretty_assertions::assert_eq;

    let server = start_mock_server().await;
    let req1 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
    )
    .await;
    let req2 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
    )
    .await;

    let TestCodex {
        codex,
        config,
        thread_manager,
        ..
    } = test_codex()
        .with_pre_build_hook(write_global_instructions)
        .with_config(|config| {
            config.model = Some("gpt-5.2".to_string());
            // Keep tool expectations stable when the default web_search mode changes.
            config
                .web_search_mode
                .set(WebSearchMode::Cached)
                .expect("test web_search_mode should satisfy constraints");
            config
                .features
                .enable(Feature::CollaborationModes)
                .expect("test config should allow feature update");
        })
        .build(&server)
        .await?;
    let base_instructions = thread_manager
        .get_models_manager()
        .get_model_info(
            config
                .model
                .as_deref()
                .expect("test config should have a model"),
            &config.to_models_manager_config(),
        )
        .await
        .base_instructions;

    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 1".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 2".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let mut expected_tools_names = if cfg!(windows) {
        vec!["shell_command"]
    } else {
        vec!["exec_command", "write_stdin"]
    };
    expected_tools_names.extend([
        "update_plan",
        "request_user_input",
        "apply_patch",
        "view_image",
        "tool_search",
        "web_search",
    ]);
    let body0 = req1.single_request().body_json();

    let expected_instructions = if expected_tools_names.contains(&"apply_patch") {
        base_instructions
    } else {
        [base_instructions, APPLY_PATCH_TOOL_INSTRUCTIONS.to_string()].join("\n")
    };

    assert_eq!(
        body0["instructions"],
        serde_json::json!(expected_instructions),
    );
    assert_tool_names(&body0, &expected_tools_names);

    let body1 = req2.single_request().body_json();
    assert_eq!(
        body1["instructions"],
        serde_json::json!(expected_instructions),
    );
    assert_tool_names(&body1, &expected_tools_names);

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> anyhow::Result<()> {
    skip_if_no_network!(Ok(()));
    use pretty_assertions::assert_eq;

    let server = start_mock_server().await;
    let req1 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
    )
    .await;
    let req2 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
    )
    .await;

    let TestCodex { codex, .. } = test_codex()
        .with_pre_build_hook(write_global_instructions)
        .with_config(|config| {
            config
                .features
                .enable(Feature::CollaborationModes)
                .expect("test config should allow feature update");
            config.model = Some("gpt-5.2".to_string());
        })
        .build(&server)
        .await?;

    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 1".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;

    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 2".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;

    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let body0 = req1.single_request().body_json();
    let instructions0 = body0["instructions"]
        .as_str()
        .expect("instructions should be a string");
    assert!(
        instructions0.contains("You are"),
        "expected non-empty instructions"
    );

    let body1 = req2.single_request().body_json();
    let instructions1 = body1["instructions"]
        .as_str()
        .expect("instructions should be a string");
    assert_eq!(
        normalize_newlines(instructions1),
        normalize_newlines(instructions0)
    );

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn prefixes_context_and_instructions_once_and_consistently_across_requests()
-> anyhow::Result<()> {
    skip_if_no_network!(Ok(()));
    use pretty_assertions::assert_eq;

    let server = start_mock_server().await;
    let req1 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
    )
    .await;
    let req2 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
    )
    .await;

    let TestCodex { codex, config, .. } = test_codex()
        .with_pre_build_hook(write_global_instructions)
        .with_config(|config| {
            config
                .features
                .enable(Feature::CollaborationModes)
                .expect("test config should allow feature update");
        })
        .build(&server)
        .await?;

    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 1".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 2".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let body1 = req1.single_request().body_json();
    let input1 = body1["input"].as_array().expect("input array");
    assert_eq!(
        input1.len(),
        3,
        "expected permissions + cached contextual user prefix + user msg"
    );

    let ui_text = input1[1]["content"][0]["text"]
        .as_str()
        .expect("ui message text");
    assert!(
        ui_text.contains("be consistent and helpful"),
        "expected user instructions in UI message: {ui_text}"
    );

    let cwd_str = config.cwd.to_string_lossy();
    let env_text = input1[1]["content"][1]["text"]
        .as_str()
        .expect("environment context text");
    assert_default_env_context(env_text, &cwd_str);
    assert_eq!(
        input1[1]["content"][1]["type"].as_str(),
        Some("input_text"),
        "expected environment context bundled after UI message in cached contextual message"
    );
    assert_eq!(input1[2], text_user_input("hello 1".to_string()));

    let body2 = req2.single_request().body_json();
    let input2 = body2["input"].as_array().expect("input array");
    assert_eq!(
        &input2[..input1.len()],
        input1.as_slice(),
        "expected cached prefix to be reused"
    );
    assert_eq!(input2[input1.len()], text_user_input("hello 2".to_string()));

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> anyhow::Result<()> {
    skip_if_no_network!(Ok(()));
    use pretty_assertions::assert_eq;

    let server = start_mock_server().await;
    let req1 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
    )
    .await;
    let req2 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
    )
    .await;

    let TestCodex { codex, config, .. } = test_codex()
        .with_pre_build_hook(write_global_instructions)
        .with_config(|config| {
            config
                .features
                .enable(Feature::CollaborationModes)
                .expect("test config should allow feature update");
        })
        .build(&server)
        .await?;

    // First turn
    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 1".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let writable = TempDir::new().unwrap();
    let permission_profile = PermissionProfile::workspace_write_with(
        &[writable.abs()],
        NetworkSandboxPolicy::Enabled,
        /*exclude_tmpdir_env_var*/ true,
        /*exclude_slash_tmp*/ true,
    );
    let sandbox_policy = permission_profile
        .to_legacy_sandbox_policy(config.cwd.as_path())
        .expect("workspace profile should have legacy projection");
    core_test_support::submit_thread_settings(
        &codex,
        codex_protocol::protocol::ThreadSettingsOverrides {
            approval_policy: Some(AskForApproval::Never),
            sandbox_policy: Some(sandbox_policy),
            permission_profile: Some(permission_profile),
            effort: Some(Some(ReasoningEffort::High)),
            summary: Some(ReasoningSummary::Detailed),
            ..Default::default()
        },
    )
    .await?;

    // Second turn after overrides
    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 2".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let request1 = req1.single_request();
    let request2 = req2.single_request();
    let body1 = request1.body_json();
    let body2 = request2.body_json();
    // prompt_cache_key should remain constant across overrides
    assert_eq!(
        body1["prompt_cache_key"], body2["prompt_cache_key"],
        "prompt_cache_key should not change across overrides"
    );

    // The entire prefix from the first request should be identical and reused
    // as the prefix of the second request, ensuring cache hit potential.
    let expected_user_message_2 = serde_json::json!({
        "type": "message",
        "role": "user",
        "content": [ { "type": "input_text", "text": "hello 2" } ]
    });
    let expected_permissions_msg = body1["input"][0].clone();
    let body1_input = body1["input"].as_array().expect("input array");
    // After overriding the thread settings, emit one updated permissions message.
    let expected_permissions_msg_2 = body2["input"][body1_input.len()].clone();
    assert_ne!(
        expected_permissions_msg_2, expected_permissions_msg,
        "expected updated permissions message after override"
    );
    let expected_env_msg_2 = body2["input"][body1_input.len() + 1].clone();
    assert_eq!(expected_env_msg_2["role"].as_str(), Some("user"));
    let env_text = expected_env_msg_2["content"][0]["text"]
        .as_str()
        .expect("environment context text");
    assert_env_context_fragment(env_text);
    assert!(
        env_text.contains("<permission_profile type=\"managed\">")
            && env_text.contains("<file_system type=\"restricted\">")
            && env_text.contains(&format!(
                "<entry access=\"write\"><path>{}</path></entry>",
                writable.abs().display()
            )),
        "expected workspace-write filesystem profile in environment context: {env_text}"
    );
    let mut expected_body2 = body1_input.to_vec();
    expected_body2.push(expected_permissions_msg_2);
    expected_body2.push(expected_env_msg_2);
    expected_body2.push(expected_user_message_2);
    assert_eq!(body2["input"], serde_json::Value::Array(expected_body2));

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn override_before_first_turn_emits_environment_context() -> anyhow::Result<()> {
    skip_if_no_network!(Ok(()));

    let server = start_mock_server().await;
    let req = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
    )
    .await;

    let TestCodex { codex, .. } = test_codex().build(&server).await?;

    let collaboration_mode = CollaborationMode {
        mode: ModeKind::Default,
        settings: Settings {
            model: "gpt-5.4".to_string(),
            reasoning_effort: Some(ReasoningEffort::High),
            developer_instructions: None,
        },
    };

    core_test_support::submit_thread_settings(
        &codex,
        codex_protocol::protocol::ThreadSettingsOverrides {
            approval_policy: Some(AskForApproval::Never),
            model: Some("gpt-5.4".to_string()),
            effort: Some(Some(ReasoningEffort::Low)),
            collaboration_mode: Some(collaboration_mode),
            ..Default::default()
        },
    )
    .await?;

    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "first message".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;

    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let body = req.single_request().body_json();
    assert_eq!(body["model"].as_str(), Some("gpt-5.4"));
    assert_eq!(
        body.get("reasoning")
            .and_then(|reasoning| reasoning.get("effort"))
            .and_then(|value| value.as_str()),
        Some("high")
    );
    let input = body["input"]
        .as_array()
        .expect("input array must be present");
    assert!(
        !input.is_empty(),
        "expected at least environment context and user message"
    );

    let env_texts: Vec<&str> = input
        .iter()
        .filter_map(|msg| {
            msg["content"].as_array().map(|content| {
                content
                    .iter()
                    .filter_map(|item| item["text"].as_str())
                    .collect::<Vec<_>>()
            })
        })
        .flatten()
        .filter(|text| text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG))
        .collect();
    assert!(
        !env_texts.is_empty(),
        "expected environment context to be emitted: {env_texts:?}"
    );
    assert!(
        env_texts
            .iter()
            .any(|text| text.contains("<current_date>") && text.contains("</current_date>")),
        "expected current_date in environment context: {env_texts:?}"
    );
    assert!(
        env_texts
            .iter()
            .any(|text| text.contains("<timezone>") && text.contains("</timezone>")),
        "expected timezone in environment context: {env_texts:?}"
    );

    let env_count = input
        .iter()
        .filter(|msg| {
            msg["content"]
                .as_array()
                .and_then(|content| {
                    content.iter().find(|item| {
                        item["type"].as_str() == Some("input_text")
                            && item["text"]
                                .as_str()
                                .map(|text| text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG))
                                .unwrap_or(false)
                    })
                })
                .is_some()
        })
        .count();
    assert!(
        env_count >= 1,
        "environment context should appear at least once, found {env_count}"
    );

    let permissions_texts: Vec<&str> = input
        .iter()
        .filter_map(|msg| {
            let role = msg["role"].as_str()?;
            if role != "developer" {
                return None;
            }
            msg["content"]
                .as_array()
                .and_then(|content| content.first())
                .and_then(|item| item["text"].as_str())
        })
        .collect();
    assert!(
        permissions_texts.iter().any(|text| {
            let lower = text.to_ascii_lowercase();
            (lower.contains("approval policy") || lower.contains("approval_policy"))
                && lower.contains("never")
        }),
        "permissions message should reflect overridden approval policy: {permissions_texts:?}"
    );

    let user_texts: Vec<&str> = input
        .iter()
        .filter_map(|msg| {
            msg["content"].as_array().map(|content| {
                content
                    .iter()
                    .filter_map(|item| item["text"].as_str())
                    .collect::<Vec<_>>()
            })
        })
        .flatten()
        .collect();
    assert!(
        user_texts.contains(&"first message"),
        "expected user message text, got {user_texts:?}"
    );

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Result<()> {
    skip_if_no_network!(Ok(()));
    use pretty_assertions::assert_eq;

    let server = start_mock_server().await;
    let req1 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
    )
    .await;
    let req2 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
    )
    .await;

    let TestCodex { codex, .. } = test_codex()
        .with_pre_build_hook(write_global_instructions)
        .with_config(|config| {
            config
                .features
                .enable(Feature::CollaborationModes)
                .expect("test config should allow feature update");
        })
        .build(&server)
        .await?;

    // First turn
    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 1".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: Default::default(),
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    // Second turn using per-turn thread-settings overrides.
    let new_cwd = TempDir::new().unwrap();
    let writable = TempDir::new().unwrap();
    let permission_profile = PermissionProfile::workspace_write_with(
        &[writable.abs()],
        NetworkSandboxPolicy::Enabled,
        /*exclude_tmpdir_env_var*/ true,
        /*exclude_slash_tmp*/ true,
    );
    let (sandbox_policy, permission_profile) =
        turn_permission_fields(permission_profile, new_cwd.path());
    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 2".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
                environments: Some(local_selections(new_cwd.abs())),
                approval_policy: Some(AskForApproval::Never),
                sandbox_policy: Some(sandbox_policy),
                permission_profile,
                model: Some("o3".to_string()),
                effort: Some(Some(ReasoningEffort::High)),
                summary: Some(ReasoningSummary::Detailed),
                ..Default::default()
            },
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let request1 = req1.single_request();
    let request2 = req2.single_request();
    let body1 = request1.body_json();
    let body2 = request2.body_json();

    // prompt_cache_key should remain constant across per-turn overrides
    assert_eq!(
        body1["prompt_cache_key"], body2["prompt_cache_key"],
        "prompt_cache_key should not change across per-turn overrides"
    );

    // The entire prefix from the first request should be identical and reused
    // as the prefix of the second request.
    let expected_user_message_2 = serde_json::json!({
        "type": "message",
        "role": "user",
        "content": [ { "type": "input_text", "text": "hello 2" } ]
    });
    let expected_permissions_msg = body1["input"][0].clone();
    let body1_input = body1["input"].as_array().expect("input array");
    let expected_settings_update_msg = body2["input"][body1_input.len()].clone();
    assert_ne!(
        expected_settings_update_msg, expected_permissions_msg,
        "expected updated permissions message after per-turn override"
    );
    assert_eq!(
        expected_settings_update_msg["role"].as_str(),
        Some("developer")
    );
    assert!(
        request2.has_message_with_input_texts("developer", |texts| {
            texts.iter().any(|text| text.contains("<model_switch>"))
        }),
        "expected model switch section after model override: {expected_settings_update_msg:?}"
    );
    let expected_env_msg_2 = body2["input"][body1_input.len() + 1].clone();
    assert_eq!(expected_env_msg_2["role"].as_str(), Some("user"));
    let env_text = expected_env_msg_2["content"][0]["text"]
        .as_str()
        .expect("environment context text");
    let expected_cwd = new_cwd.path().display().to_string();
    assert_default_env_context(env_text, &expected_cwd);
    let mut expected_body2 = body1_input.to_vec();
    expected_body2.push(expected_settings_update_msg);
    expected_body2.push(expected_env_msg_2);
    expected_body2.push(expected_user_message_2);
    assert_eq!(body2["input"], serde_json::Value::Array(expected_body2));

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> anyhow::Result<()> {
    skip_if_no_network!(Ok(()));
    use pretty_assertions::assert_eq;

    let server = start_mock_server().await;
    let req1 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
    )
    .await;
    let req2 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
    )
    .await;

    let TestCodex {
        codex,
        config,
        session_configured,
        ..
    } = test_codex()
        .with_pre_build_hook(write_global_instructions)
        .with_config(|config| {
            config
                .features
                .enable(Feature::CollaborationModes)
                .expect("test config should allow feature update");
        })
        .build(&server)
        .await?;

    let default_cwd = config.cwd.clone();
    let default_approval_policy = config.permissions.approval_policy.value();
    let default_sandbox_policy = &config.legacy_sandbox_policy();
    let default_model = session_configured.model;
    let default_effort = config.model_reasoning_effort.clone();
    let default_summary = config.model_reasoning_summary;

    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 1".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
                environments: Some(local_selections(default_cwd.clone())),
                approval_policy: Some(default_approval_policy),
                sandbox_policy: Some(default_sandbox_policy.clone()),
                summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
                collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
                    mode: codex_protocol::config_types::ModeKind::Default,
                    settings: codex_protocol::config_types::Settings {
                        model: default_model.clone(),
                        reasoning_effort: default_effort.clone(),
                        developer_instructions: None,
                    },
                }),
                ..Default::default()
            },
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 2".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
                environments: Some(local_selections(default_cwd.clone())),
                approval_policy: Some(default_approval_policy),
                sandbox_policy: Some(default_sandbox_policy.clone()),
                summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
                collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
                    mode: codex_protocol::config_types::ModeKind::Default,
                    settings: codex_protocol::config_types::Settings {
                        model: default_model.clone(),
                        reasoning_effort: default_effort,
                        developer_instructions: None,
                    },
                }),
                ..Default::default()
            },
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let request1 = req1.single_request();
    let request2 = req2.single_request();
    let body1 = request1.body_json();
    let body2 = request2.body_json();

    let expected_permissions_msg = body1["input"][0].clone();
    let expected_ui_msg = body1["input"][1].clone();

    let default_cwd_lossy = default_cwd.to_string_lossy();
    let expected_env_text_1 = expected_ui_msg["content"][1]["text"]
        .as_str()
        .expect("cached environment context text")
        .to_string();
    assert_default_env_context(&expected_env_text_1, &default_cwd_lossy);

    let expected_contextual_user_msg_1 = text_user_input_parts(vec![
        expected_ui_msg["content"][0]["text"]
            .as_str()
            .expect("cached user instructions text")
            .to_string(),
        expected_env_text_1,
    ]);
    let expected_user_message_1 = text_user_input("hello 1".to_string());

    let expected_input_1 = serde_json::Value::Array(vec![
        expected_permissions_msg.clone(),
        expected_contextual_user_msg_1.clone(),
        expected_user_message_1.clone(),
    ]);
    assert_eq!(body1["input"], expected_input_1);

    let expected_user_message_2 = text_user_input("hello 2".to_string());
    let expected_input_2 = serde_json::Value::Array(vec![
        expected_permissions_msg,
        expected_contextual_user_msg_1,
        expected_user_message_1,
        expected_user_message_2,
    ]);
    assert_eq!(body2["input"], expected_input_2);

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Result<()> {
    skip_if_no_network!(Ok(()));
    use pretty_assertions::assert_eq;

    let server = start_mock_server().await;

    let req1 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
    )
    .await;
    let req2 = mount_sse_once(
        &server,
        sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
    )
    .await;
    let TestCodex {
        codex,
        config,
        session_configured,
        ..
    } = test_codex()
        .with_pre_build_hook(write_global_instructions)
        .with_config(|config| {
            config
                .features
                .enable(Feature::CollaborationModes)
                .expect("test config should allow feature update");
        })
        .build(&server)
        .await?;

    let default_cwd = config.cwd.clone();
    let default_approval_policy = config.permissions.approval_policy.value();
    let default_sandbox_policy = &config.legacy_sandbox_policy();
    let default_model = session_configured.model;
    let default_effort = config.model_reasoning_effort.clone();
    let default_summary = config.model_reasoning_summary;

    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 1".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
                environments: Some(local_selections(default_cwd.clone())),
                approval_policy: Some(default_approval_policy),
                sandbox_policy: Some(default_sandbox_policy.clone()),
                summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
                collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
                    mode: codex_protocol::config_types::ModeKind::Default,
                    settings: codex_protocol::config_types::Settings {
                        model: default_model,
                        reasoning_effort: default_effort,
                        developer_instructions: None,
                    },
                }),
                ..Default::default()
            },
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let (sandbox_policy, permission_profile) =
        turn_permission_fields(PermissionProfile::Disabled, default_cwd.as_path());
    codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: "hello 2".into(),
                text_elements: Vec::new(),
            }],
            final_output_json_schema: None,
            responsesapi_client_metadata: None,
            additional_context: Default::default(),
            thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
                environments: Some(local_selections(default_cwd.clone())),
                approval_policy: Some(AskForApproval::Never),
                sandbox_policy: Some(sandbox_policy),
                permission_profile,
                summary: Some(ReasoningSummary::Detailed),
                collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
                    mode: codex_protocol::config_types::ModeKind::Default,
                    settings: codex_protocol::config_types::Settings {
                        model: "o3".to_string(),
                        reasoning_effort: Some(ReasoningEffort::High),
                        developer_instructions: None,
                    },
                }),
                ..Default::default()
            },
        })
        .await?;
    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;

    let request1 = req1.single_request();
    let request2 = req2.single_request();
    let body1 = request1.body_json();
    let body2 = request2.body_json();

    let expected_permissions_msg = body1["input"][0].clone();
    let expected_ui_msg = body1["input"][1].clone();

    let expected_env_text_1 = expected_ui_msg["content"][1]["text"]
        .as_str()
        .expect("cached environment context text")
        .to_string();
    assert_default_env_context(&expected_env_text_1, &default_cwd.to_string_lossy());
    let expected_contextual_user_msg_1 = text_user_input_parts(vec![
        expected_ui_msg["content"][0]["text"]
            .as_str()
            .expect("cached user instructions text")
            .to_string(),
        expected_env_text_1,
    ]);
    let expected_user_message_1 = text_user_input("hello 1".to_string());
    let expected_input_1 = serde_json::Value::Array(vec![
        expected_permissions_msg.clone(),
        expected_contextual_user_msg_1.clone(),
        expected_user_message_1.clone(),
    ]);
    assert_eq!(body1["input"], expected_input_1);

    let body1_input = body1["input"].as_array().expect("input array");
    let expected_settings_update_msg = body2["input"][body1_input.len()].clone();
    assert_ne!(
        expected_settings_update_msg, expected_permissions_msg,
        "expected updated permissions message after policy change"
    );
    assert_eq!(
        expected_settings_update_msg["role"].as_str(),
        Some("developer")
    );
    assert!(
        request2.has_message_with_input_texts("developer", |texts| {
            texts.iter().any(|text| text.contains("<model_switch>"))
        }),
        "expected model switch section after model override: {expected_settings_update_msg:?}"
    );
    let expected_env_update_msg = body2["input"][body1_input.len() + 1].clone();
    assert_eq!(expected_env_update_msg["role"].as_str(), Some("user"));
    let expected_env_update_text = expected_env_update_msg["content"][0]["text"]
        .as_str()
        .expect("environment context text");
    assert_env_context_fragment(expected_env_update_text);
    assert!(
        expected_env_update_text.contains(
            "<permission_profile type=\"disabled\"><file_system type=\"unrestricted\" /></permission_profile>",
        ),
        "expected disabled filesystem profile in environment context: {expected_env_update_text}"
    );
    let expected_user_message_2 = text_user_input("hello 2".to_string());
    let expected_input_2 = serde_json::Value::Array(vec![
        expected_permissions_msg,
        expected_contextual_user_msg_1,
        expected_user_message_1,
        expected_settings_update_msg,
        expected_env_update_msg,
        expected_user_message_2,
    ]);
    assert_eq!(body2["input"], expected_input_2);

    Ok(())
}
