//! Verifies that parent and spawned subagent Responses API requests carry the expected window,
//! parent-thread, and subagent identity headers.

use anyhow::Result;
use anyhow::anyhow;
use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::ResponseMock;
use core_test_support::responses::ResponsesRequest;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_function_call_with_namespace;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once_match;
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 pretty_assertions::assert_eq;
use serde_json::json;
use std::time::Duration;

const PARENT_PROMPT: &str = "spawn a subagent and report when it is started";
const CHILD_PROMPT: &str = "child: say done";
const SPAWN_CALL_ID: &str = "spawn-call-1";
const REQUEST_POLL_INTERVAL: Duration = Duration::from_millis(/*millis*/ 20);
const TURN_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 60);
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn responses_api_parent_and_subagent_requests_include_identity_headers() -> Result<()> {
    skip_if_no_network!(Ok(()));

    let server = start_mock_server().await;

    let spawn_args = serde_json::to_string(&json!({ "message": CHILD_PROMPT }))?;
    let parent_mock = mount_sse_once_match(
        &server,
        |req: &wiremock::Request| {
            request_body_contains(req, PARENT_PROMPT)
                && request_header(req, "x-openai-subagent").is_none()
        },
        sse(vec![
            ev_response_created("resp-parent-1"),
            ev_function_call_with_namespace(
                SPAWN_CALL_ID,
                "multi_agent_v1",
                "spawn_agent",
                &spawn_args,
            ),
            ev_completed("resp-parent-1"),
        ]),
    )
    .await;
    let child_mock = mount_sse_once_match(
        &server,
        |req: &wiremock::Request| {
            request_body_contains(req, CHILD_PROMPT)
                && !request_body_contains(req, SPAWN_CALL_ID)
                && request_header(req, "x-openai-subagent") == Some("collab_spawn")
        },
        sse(vec![
            ev_response_created("resp-child-1"),
            ev_assistant_message("msg-child-1", "child done"),
            ev_completed("resp-child-1"),
        ]),
    )
    .await;
    mount_sse_once_match(
        &server,
        |req: &wiremock::Request| {
            request_body_contains(req, SPAWN_CALL_ID)
                && request_header(req, "x-openai-subagent").is_none()
        },
        sse(vec![
            ev_response_created("resp-parent-2"),
            ev_assistant_message("msg-parent-2", "parent done"),
            ev_completed("resp-parent-2"),
        ]),
    )
    .await;

    let mut builder = test_codex().with_config(|config| {
        config
            .features
            .disable(Feature::EnableRequestCompression)
            .expect("test config should allow feature update");
    });
    let test = builder.build(&server).await?;
    submit_turn_with_timeout(&test, PARENT_PROMPT).await?;

    let parent = wait_for_matching_request(&parent_mock, "parent request", |request| {
        request.body_contains_text(PARENT_PROMPT) && request.header("x-openai-subagent").is_none()
    })
    .await?;
    let child = wait_for_matching_request(&child_mock, "child request", |request| {
        request.body_contains_text(CHILD_PROMPT)
            && !request.body_contains_text(SPAWN_CALL_ID)
            && request.header("x-openai-subagent").as_deref() == Some("collab_spawn")
    })
    .await?;

    let parent_window_id = parent
        .header("x-codex-window-id")
        .ok_or_else(|| anyhow!("parent request missing x-codex-window-id"))?;
    let child_window_id = child
        .header("x-codex-window-id")
        .ok_or_else(|| anyhow!("child request missing x-codex-window-id"))?;
    let (parent_thread_id, parent_generation) = split_window_id(&parent_window_id)?;
    let (child_thread_id, child_generation) = split_window_id(&child_window_id)?;

    assert_eq!(parent_generation, 0);
    assert_eq!(child_generation, 0);
    assert!(child_thread_id != parent_thread_id);
    assert_eq!(parent.header("x-openai-subagent"), None);
    assert_eq!(
        child.header("x-openai-subagent").as_deref(),
        Some("collab_spawn")
    );
    assert_eq!(
        child.header("x-codex-parent-thread-id").as_deref(),
        Some(parent_thread_id)
    );
    let child_turn_metadata: serde_json::Value = serde_json::from_str(
        &child
            .header("x-codex-turn-metadata")
            .ok_or_else(|| anyhow!("child request missing x-codex-turn-metadata"))?,
    )?;
    assert!(child_turn_metadata.get("forked_from_thread_id").is_none());
    assert_eq!(
        child_turn_metadata["parent_thread_id"].as_str(),
        Some(parent_thread_id)
    );

    Ok(())
}

async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()> {
    let session_model = test.session_configured.model.clone();
    let cwd = test.config.cwd.clone();
    let (sandbox_policy, permission_profile) =
        turn_permission_fields(PermissionProfile::workspace_write(), cwd.as_path());
    test.codex
        .submit(Op::UserInput {
            items: vec![UserInput::Text {
                text: prompt.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(cwd)),
                approval_policy: Some(AskForApproval::OnRequest),
                sandbox_policy: Some(sandbox_policy),
                permission_profile,
                collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
                    mode: codex_protocol::config_types::ModeKind::Default,
                    settings: codex_protocol::config_types::Settings {
                        model: session_model,
                        reasoning_effort: None,
                        developer_instructions: None,
                    },
                }),
                ..Default::default()
            },
        })
        .await?;

    let turn_started = wait_for_event_result(test, "turn started", |event| {
        matches!(event, EventMsg::TurnStarted(_))
    })
    .await?;
    let EventMsg::TurnStarted(turn_started) = turn_started else {
        unreachable!("event predicate only matches turn started events");
    };
    wait_for_event_result(test, "turn complete", |event| match event {
        EventMsg::TurnComplete(event) => event.turn_id == turn_started.turn_id,
        _ => false,
    })
    .await?;

    Ok(())
}

async fn wait_for_matching_request<F>(
    mock: &ResponseMock,
    label: &str,
    mut predicate: F,
) -> Result<ResponsesRequest>
where
    F: FnMut(&ResponsesRequest) -> bool,
{
    tokio::time::timeout(TURN_TIMEOUT, async {
        loop {
            if let Some(request) = mock
                .requests()
                .into_iter()
                .find(|request| predicate(request))
            {
                return request;
            }
            tokio::time::sleep(REQUEST_POLL_INTERVAL).await;
        }
    })
    .await
    .map_err(|_| anyhow!("timed out waiting for {label}"))
}

async fn wait_for_event_result<F>(
    test: &TestCodex,
    stage: &str,
    mut predicate: F,
) -> Result<EventMsg>
where
    F: FnMut(&EventMsg) -> bool,
{
    let mut seen_events = Vec::new();
    tokio::time::timeout(TURN_TIMEOUT, async {
        loop {
            let event = test.codex.next_event().await?;
            seen_events.push(event_summary(&event.msg));
            if predicate(&event.msg) {
                return Ok::<EventMsg, anyhow::Error>(event.msg);
            }
        }
    })
    .await
    .map_err(|_| {
        anyhow!(
            "timed out waiting for {stage}; saw events: {}",
            seen_events.join(" | ")
        )
    })?
}

fn event_summary(event: &EventMsg) -> String {
    let mut summary = format!("{event:?}");
    summary.truncate(240);
    summary
}

fn request_body_contains(req: &wiremock::Request, text: &str) -> bool {
    std::str::from_utf8(&req.body).is_ok_and(|body| body.contains(text))
}

fn request_header<'a>(req: &'a wiremock::Request, name: &str) -> Option<&'a str> {
    req.headers.get(name).and_then(|value| value.to_str().ok())
}

fn split_window_id(window_id: &str) -> Result<(&str, u64)> {
    let (thread_id, generation) = window_id
        .rsplit_once(':')
        .ok_or_else(|| anyhow!("invalid window id header: {window_id}"))?;
    Ok((thread_id, generation.parse::<u64>()?))
}
