use std::collections::HashSet;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration;

use http::HeaderMap;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::sync::oneshot::error::TryRecvError;
use url::Url;

use crate::CompactionRequest;
use crate::CompactionResponse;
use crate::CredentialSource;
use crate::MemorySummarizeRequest;
use crate::MemorySummarizeResponse;
use crate::ModelInfo;
use crate::ProviderCredentials;
use crate::ProviderDefinition;
use crate::ProviderError;
use crate::ProviderEvent;
use crate::ProviderEventStream;
use crate::ProviderSession;
use crate::ReasoningFormat;
use crate::ReasoningProvenance;
use crate::Request;
use crate::Response;
use crate::ResponsesTransport;
use crate::SessionRequestOptions;
use crate::request::ResponsesRequestCompression;

use super::SharedTurnState;
use super::model::ResponsesModelsPage;
use super::model::ResponsesRequest;
use super::sse::spawn_event_stream_with_provenance;
#[cfg(feature = "responses-websocket")]
use super::websocket::ResponsesWebsocketConnection;
#[cfg(feature = "responses-websocket")]
use super::websocket::ResponsesWebsocketTelemetry;
#[cfg(feature = "responses-websocket")]
use super::websocket::merge_request_headers;
#[cfg(feature = "responses-websocket")]
use super::websocket::response_create_frame;

/// Session-scoped Responses transport state.
///
/// This is intentionally lightweight and keeps the pieces needed for websocket prewarm and
/// HTTP fallback without binding the provider to any higher-level runtime.
pub struct ResponsesSession<C> {
    definition: ProviderDefinition,
    credential_source: Arc<C>,
    client: reqwest::Client,
    state: Arc<ResponsesSessionState>,
    endpoint_capabilities: Arc<ResponsesEndpointCapabilities>,
    hybrid_http_previous_response_id: bool,
}

#[derive(Default)]
struct WebsocketSession {
    connection_reused: StdMutex<bool>,
    #[cfg(feature = "responses-websocket")]
    connection: Option<ResponsesWebsocketConnection>,
    _last_request: Option<ResponsesRequest>,
    last_response_rx: Option<oneshot::Receiver<Response>>,
}

impl WebsocketSession {
    fn set_connection_reused(&self, connection_reused: bool) {
        *self
            .connection_reused
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = connection_reused;
    }

    fn connection_reused(&self) -> bool {
        *self
            .connection_reused
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    #[cfg(feature = "responses-websocket")]
    fn connection(&self) -> Option<ResponsesWebsocketConnection> {
        self.connection.clone()
    }

    #[cfg(feature = "responses-websocket")]
    fn store_connection(&mut self, connection: ResponsesWebsocketConnection) {
        self.connection = Some(connection);
    }

    #[cfg(feature = "responses-websocket")]
    fn clear_connection(&mut self) {
        self.connection = None;
    }
}

pub(crate) struct ResponsesSessionState {
    disable_websockets: AtomicBool,
    websocket_session: StdMutex<WebsocketSession>,
    turn_state: SharedTurnState,
    latest_response_id: StdMutex<Option<String>>,
}

impl Default for ResponsesSessionState {
    fn default() -> Self {
        Self {
            disable_websockets: AtomicBool::new(false),
            websocket_session: StdMutex::new(WebsocketSession::default()),
            turn_state: Arc::new(StdMutex::new(None)),
            latest_response_id: StdMutex::new(None),
        }
    }
}

impl ResponsesSessionState {
    fn turn_state(&self) -> Option<String> {
        self.turn_state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    fn set_turn_state(&self, turn_state: impl Into<String>) {
        *self
            .turn_state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(turn_state.into());
    }

    fn latest_response_id(&self) -> Option<String> {
        self.latest_response_id
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }

    fn set_latest_response_id(&self, response_id: impl Into<String>) {
        *self
            .latest_response_id
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(response_id.into());
    }

    fn clear_latest_response_id(&self) {
        *self
            .latest_response_id
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
    }
}

#[derive(Default)]
pub(crate) struct ResponsesEndpointCapabilities {
    http_previous_response_id_unsupported_models: StdMutex<HashSet<String>>,
}

impl ResponsesEndpointCapabilities {
    fn http_previous_response_id_is_unsupported(&self, model: &str) -> bool {
        self.http_previous_response_id_unsupported_models
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .contains(model)
    }

    fn mark_http_previous_response_id_unsupported(&self, model: impl Into<String>) {
        self.http_previous_response_id_unsupported_models
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert(model.into());
    }
}

impl<C> ResponsesSession<C>
where
    C: CredentialSource + 'static,
{
    pub(crate) fn new(
        definition: ProviderDefinition,
        credential_source: Arc<C>,
        client: reqwest::Client,
        state: Arc<ResponsesSessionState>,
        endpoint_capabilities: Arc<ResponsesEndpointCapabilities>,
        hybrid_http_previous_response_id: bool,
    ) -> Self {
        Self {
            definition,
            credential_source,
            client,
            state,
            endpoint_capabilities,
            hybrid_http_previous_response_id,
        }
    }

    pub fn websocket_connect_timeout(&self) -> Duration {
        self.definition.websocket_connect_timeout
    }

    pub fn stream_idle_timeout(&self) -> Duration {
        self.definition.stream_idle_timeout
    }

    pub fn websocket_url_for_path(&self, path: &str) -> Result<Url, ProviderError> {
        self.definition
            .websocket_url_for_path(path)
            .map_err(|error| ProviderError::InvalidRequest(error.to_string()))
    }

    pub fn request_url_for_path(&self, path: &str) -> Result<Url, ProviderError> {
        Url::parse(&self.definition.url_for_path(path))
            .map_err(|error| ProviderError::InvalidRequest(error.to_string()))
    }

    fn responses_endpoint_path(&self) -> &'static str {
        responses_endpoint_path_for_base(self.definition.base_url.as_deref())
    }

    pub fn disable_websockets(&self) {
        self.state.disable_websockets.store(true, Ordering::Relaxed);
    }

    pub fn websockets_enabled(&self) -> bool {
        self.definition.capabilities.supports_websockets
            && !self.state.disable_websockets.load(Ordering::Relaxed)
    }

    pub fn set_connection_reused(&self, connection_reused: bool) {
        self.state
            .websocket_session
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .set_connection_reused(connection_reused);
    }

    pub fn connection_reused(&self) -> bool {
        self.state
            .websocket_session
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .connection_reused()
    }

    pub fn build_websocket_headers(
        &self,
        credentials: &ProviderCredentials,
        turn_metadata_header: Option<&str>,
    ) -> Result<HeaderMap, ProviderError> {
        let session = SessionRequestOptions {
            sticky_turn_state: self.state.turn_state(),
            turn_metadata: turn_metadata_header.map(str::to_string),
            subagent: None,
            prefer_connection_reuse: Some(self.connection_reused()),
            session_affinity: None,
            extra_headers: std::collections::BTreeMap::new(),
        };
        self.build_websocket_headers_for_session(credentials, Some(&session))
    }

    pub fn build_websocket_headers_for_session(
        &self,
        credentials: &ProviderCredentials,
        session: Option<&SessionRequestOptions>,
    ) -> Result<HeaderMap, ProviderError> {
        let fallback_turn_state = self.state.turn_state();
        self.definition.build_headers_for_session(
            credentials,
            session,
            fallback_turn_state.as_deref(),
        )
    }

    pub fn set_turn_state(&self, turn_state: impl Into<String>) -> bool {
        self.state.set_turn_state(turn_state);
        true
    }

    pub fn turn_state(&self) -> Option<String> {
        self.state.turn_state()
    }

    pub fn latest_response_id(&self) -> Option<String> {
        self.state.latest_response_id()
    }

    pub fn clear_latest_response_id(&self) {
        self.state.clear_latest_response_id();
    }

    /// Whether the cached websocket connection is gone or was never opened.
    ///
    /// This and the four methods below are the session's half of the
    /// `responses-websocket` transport; without the feature there is no
    /// connection to cache, so they are not compiled.
    #[cfg(feature = "responses-websocket")]
    pub async fn websocket_connection_is_closed(&self) -> bool {
        let connection = self
            .state
            .websocket_session
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .connection();
        match connection {
            Some(connection) => connection.is_closed().await,
            None => true,
        }
    }

    #[cfg(feature = "responses-websocket")]
    pub async fn connect_websocket(
        &self,
        extra_headers: HeaderMap,
        default_headers: HeaderMap,
        turn_state: Option<SharedTurnState>,
        telemetry: Option<Arc<dyn ResponsesWebsocketTelemetry>>,
    ) -> Result<(), ProviderError> {
        let credentials = self.credential_source.credentials().await?;
        let provider_headers = self.definition.build_headers(&credentials)?;
        let headers = merge_request_headers(&provider_headers, extra_headers, default_headers);
        let url = self
            .definition
            .websocket_url_with_auth_for_path(self.responses_endpoint_path(), &credentials)?;
        let connection = ResponsesWebsocketConnection::connect(
            url,
            headers,
            turn_state.or_else(|| Some(Arc::clone(&self.state.turn_state))),
            self.stream_idle_timeout(),
            telemetry,
        )
        .await?;
        self.state
            .websocket_session
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .store_connection(connection);
        Ok(())
    }

    #[cfg(feature = "responses-websocket")]
    pub async fn stream_websocket_request(
        &self,
        request_body: serde_json::Value,
    ) -> Result<ProviderEventStream, ProviderError> {
        let (connection, connection_reused) = {
            let websocket_session = self
                .state
                .websocket_session
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            (
                websocket_session.connection(),
                websocket_session.connection_reused(),
            )
        };
        let Some(connection) = connection else {
            return Err(ProviderError::MalformedStream(
                "websocket connection is unavailable".to_string(),
            ));
        };
        connection
            .stream_request(request_body, connection_reused)
            .await
    }

    #[cfg(feature = "responses-websocket")]
    async fn stream_websocket_request_with_provenance(
        &self,
        request_body: serde_json::Value,
        provenance: ReasoningProvenance,
    ) -> Result<ProviderEventStream, ProviderError> {
        let (connection, connection_reused) = {
            let websocket_session = self
                .state
                .websocket_session
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            (
                websocket_session.connection(),
                websocket_session.connection_reused(),
            )
        };
        let Some(connection) = connection else {
            return Err(ProviderError::MalformedStream(
                "websocket connection is unavailable".to_string(),
            ));
        };
        connection
            .stream_request_with_provenance(request_body, connection_reused, provenance)
            .await
    }

    #[cfg(feature = "responses-websocket")]
    pub fn clear_websocket_connection(&self) {
        self.state
            .websocket_session
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clear_connection();
    }

    pub async fn list_models(&self) -> Result<Vec<ModelInfo>, ProviderError> {
        let credentials = self.credential_source.credentials().await?;
        let response = self
            .client
            .get(
                self.definition
                    .request_url_with_auth_for_path("v1/models", &credentials)?,
            )
            .headers(self.definition.build_headers(&credentials)?)
            .send()
            .await
            .map_err(ProviderError::Transport)?;

        if !response.status().is_success() {
            return Err(ProviderError::from_http_response(response).await);
        }

        let models = response
            .json::<ResponsesModelsPage>()
            .await
            .map_err(ProviderError::Decode)?;

        Ok(models.into_model_info(self.definition.descriptor.id.clone()))
    }

    pub async fn stream_response<'a>(
        &self,
        mut request: Request<'a>,
    ) -> Result<ProviderEventStream, ProviderError> {
        let provider_name = self
            .definition
            .descriptor
            .display_name
            .as_deref()
            .unwrap_or(self.definition.descriptor.id.as_str());
        let target_provider = self.definition.descriptor.id.clone();
        let requested_model = request.model.to_string();
        let reasoning_provenance = ReasoningProvenance {
            provider: target_provider.clone(),
            model: requested_model.clone(),
            format: ReasoningFormat::OpenAiEncrypted,
        };
        let session = request.provider_request_options.session.clone();
        let state_mode = request.provider_request_options.responses.state_mode;
        let transport = request.provider_request_options.responses.transport;
        if request
            .provider_request_options
            .responses
            .previous_response_id
            .is_none()
            && state_mode.uses_provider_state()
            && !(state_mode == crate::ResponsesStateMode::Hybrid
                && transport == ResponsesTransport::HttpSse
                && (!self.hybrid_http_previous_response_id
                    || self
                        .endpoint_capabilities
                        .http_previous_response_id_is_unsupported(&requested_model)))
        {
            request
                .provider_request_options
                .responses
                .previous_response_id = self.state.latest_response_id();
        }
        let compression = request.provider_request_options.responses.compression;
        let request = ResponsesRequest::try_from_request_with_provider(
            request,
            provider_name,
            &target_provider,
        )?;
        let credentials = self.credential_source.credentials().await?;

        match transport {
            ResponsesTransport::HttpSse => {
                self.stream_http_response(
                    request,
                    compression,
                    &credentials,
                    &session,
                    state_mode,
                    reasoning_provenance,
                )
                .await
            }
            ResponsesTransport::WebSocket => {
                self.stream_websocket_response(
                    request,
                    &credentials,
                    &session,
                    reasoning_provenance,
                )
                .await
            }
        }
    }

    async fn stream_http_response(
        &self,
        mut request: ResponsesRequest,
        compression: ResponsesRequestCompression,
        credentials: &ProviderCredentials,
        session: &SessionRequestOptions,
        state_mode: crate::ResponsesStateMode,
        reasoning_provenance: ReasoningProvenance,
    ) -> Result<ProviderEventStream, ProviderError> {
        let response = self
            .send_http_responses_request(&request, compression, credentials, session)
            .await?;

        if !response.status().is_success() {
            let error = ProviderError::from_http_response(response).await;
            if state_mode == crate::ResponsesStateMode::Hybrid
                && request.previous_response_id().is_some()
                && let Some(rejection) = previous_response_state_rejection(&error)
            {
                if rejection == PreviousResponseStateRejection::ParameterUnsupported {
                    self.endpoint_capabilities
                        .mark_http_previous_response_id_unsupported(
                            reasoning_provenance.model.clone(),
                        );
                }
                self.state.clear_latest_response_id();
                request.clear_previous_response_id();
                let response = self
                    .send_http_responses_request(&request, compression, credentials, session)
                    .await?;
                if !response.status().is_success() {
                    return Err(ProviderError::from_http_response(response).await);
                }
                return Ok(
                    self.track_response_state(spawn_event_stream_with_provenance(
                        response,
                        reasoning_provenance.provider,
                        reasoning_provenance.model,
                    )),
                );
            }
            return Err(error);
        }

        if let Some(turn_state) = response
            .headers()
            .get("x-codex-turn-state")
            .and_then(|value| value.to_str().ok())
        {
            self.state.set_turn_state(turn_state);
        }

        Ok(
            self.track_response_state(spawn_event_stream_with_provenance(
                response,
                reasoning_provenance.provider,
                reasoning_provenance.model,
            )),
        )
    }

    #[cfg(feature = "responses-websocket")]
    async fn stream_websocket_response(
        &self,
        request: ResponsesRequest,
        credentials: &ProviderCredentials,
        session: &SessionRequestOptions,
        reasoning_provenance: ReasoningProvenance,
    ) -> Result<ProviderEventStream, ProviderError> {
        if !self.websockets_enabled() {
            return Err(ProviderError::UnsupportedCapability(
                "responses_websocket".to_string(),
            ));
        }

        if self.websocket_connection_is_closed().await {
            let headers = self.build_websocket_headers_for_session(credentials, Some(session))?;
            let url = self
                .definition
                .websocket_url_with_auth_for_path(self.responses_endpoint_path(), credentials)?;
            let connection = ResponsesWebsocketConnection::connect(
                url,
                headers,
                Some(Arc::clone(&self.state.turn_state)),
                self.stream_idle_timeout(),
                None,
            )
            .await?;
            self.state
                .websocket_session
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .store_connection(connection);
        }

        let response = serde_json::to_value(request).map_err(ProviderError::Serialize)?;
        let stream = self
            .stream_websocket_request_with_provenance(
                response_create_frame(response),
                reasoning_provenance,
            )
            .await?;
        Ok(self.track_response_state(stream))
    }

    /// The same entry point when the transport was not compiled in.
    ///
    /// Selecting [`ResponsesTransport::WebSocket`] is an explicit choice by the
    /// caller, so a build without the transport reports that it cannot honor it
    /// and names the feature that would. Quietly answering over HTTP+SSE instead
    /// would return a stream the caller never asked for, and hide a
    /// misconfigured build behind a working one.
    #[cfg(not(feature = "responses-websocket"))]
    async fn stream_websocket_response(
        &self,
        _request: ResponsesRequest,
        _credentials: &ProviderCredentials,
        _session: &SessionRequestOptions,
        _reasoning_provenance: ReasoningProvenance,
    ) -> Result<ProviderEventStream, ProviderError> {
        Err(ProviderError::UnsupportedCapability(
            "responses_websocket (not compiled in: rebuild mentra-provider with the \
             `responses-websocket` feature)"
                .to_string(),
        ))
    }

    pub async fn send_response<'a>(&self, request: Request<'a>) -> Result<Response, ProviderError> {
        crate::collect_response_from_stream(self.stream_response(request).await?).await
    }

    async fn send_http_responses_request(
        &self,
        request: &ResponsesRequest,
        compression: ResponsesRequestCompression,
        credentials: &ProviderCredentials,
        session: &SessionRequestOptions,
    ) -> Result<reqwest::Response, ProviderError> {
        let request_builder = self
            .client
            .post(
                self.definition
                    .request_url_with_auth_for_path(self.responses_endpoint_path(), credentials)?,
            )
            .headers(self.build_http_headers_for_session(credentials, Some(session))?)
            .header(reqwest::header::ACCEPT, "text/event-stream");

        match compression {
            ResponsesRequestCompression::None => request_builder
                .json(request)
                .send()
                .await
                .map_err(ProviderError::Transport),
            ResponsesRequestCompression::Zstd => {
                let body = serde_json::to_vec(request).map_err(ProviderError::Serialize)?;
                let compressed =
                    zstd::stream::encode_all(std::io::Cursor::new(body), 3).map_err(|error| {
                        ProviderError::InvalidRequest(format!(
                            "failed to compress responses request: {error}"
                        ))
                    })?;
                request_builder
                    .header(reqwest::header::CONTENT_ENCODING, "zstd")
                    .header(reqwest::header::CONTENT_TYPE, "application/json")
                    .body(compressed)
                    .send()
                    .await
                    .map_err(ProviderError::Transport)
            }
        }
    }

    fn track_response_state(&self, mut stream: ProviderEventStream) -> ProviderEventStream {
        let (tx, rx) = mpsc::unbounded_channel();
        let state = Arc::clone(&self.state);

        tokio::spawn(async move {
            while let Some(event) = stream.recv().await {
                if let Ok(ProviderEvent::MessageStarted { id, .. }) = &event {
                    state.set_latest_response_id(id.clone());
                }

                if tx.send(event).is_err() {
                    break;
                }
            }
        });

        rx
    }

    pub fn take_turn_state(&self) -> SharedTurnState {
        Arc::clone(&self.state.turn_state)
    }

    pub fn last_response_rx_ready(&self) -> bool {
        let mut session = self
            .state
            .websocket_session
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        session
            .last_response_rx
            .as_mut()
            .is_some_and(|rx| matches!(rx.try_recv(), Ok(_) | Err(TryRecvError::Closed)))
    }

    fn build_http_headers_for_session(
        &self,
        credentials: &ProviderCredentials,
        session: Option<&SessionRequestOptions>,
    ) -> Result<HeaderMap, ProviderError> {
        let mut headers = self.build_websocket_headers_for_session(credentials, session)?;
        if let Some(value) = session
            .and_then(|session| session.session_affinity.as_deref())
            .and_then(|session_id| http::HeaderValue::from_str(session_id).ok())
        {
            headers.insert("x-client-request-id", value.clone());
            headers.insert("session_id", value);
        }
        if let Some(value) = session
            .and_then(|session| session.subagent.as_deref())
            .and_then(|subagent| http::HeaderValue::from_str(subagent).ok())
        {
            headers.insert("x-openai-subagent", value);
        }
        Ok(headers)
    }
}

fn responses_endpoint_path_for_base(base_url: Option<&str>) -> &'static str {
    let Some(base_url) = base_url else {
        return "v1/responses";
    };
    let Ok(url) = Url::parse(base_url) else {
        return "v1/responses";
    };

    let path = url.path().trim_end_matches('/');
    if path.ends_with("/v1") || path == "/backend-api/codex" {
        "responses"
    } else {
        "v1/responses"
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PreviousResponseStateRejection {
    ReferenceUnavailable,
    ParameterUnsupported,
}

fn previous_response_parameter_is_unsupported(body: &str) -> bool {
    let normalized = body
        .chars()
        .map(|character| {
            if character.is_ascii_alphanumeric() || character == '_' {
                character.to_ascii_lowercase()
            } else {
                ' '
            }
        })
        .collect::<String>();
    let words = normalized.split_whitespace().collect::<Vec<_>>();

    words.windows(3).any(|window| {
        matches!(
            window,
            [
                "unsupported" | "unknown" | "unrecognized",
                "parameter",
                "previous_response_id"
            ]
        )
    }) || words.windows(5).any(|window| {
        matches!(
            window,
            [
                "parameter",
                "previous_response_id",
                "is",
                "not",
                "supported"
            ] | [
                "previous_response_id",
                "parameter",
                "is",
                "not",
                "supported"
            ]
        )
    }) || words
        .windows(4)
        .any(|window| matches!(window, ["previous_response_id", "is", "not", "supported"]))
}

fn previous_response_state_rejection(
    error: &ProviderError,
) -> Option<PreviousResponseStateRejection> {
    let ProviderError::Http { status, body, .. } = error else {
        return None;
    };
    if !(*status == reqwest::StatusCode::BAD_REQUEST || *status == reqwest::StatusCode::NOT_FOUND) {
        return None;
    }

    let body = body.to_ascii_lowercase();
    if previous_response_parameter_is_unsupported(&body) {
        return Some(PreviousResponseStateRejection::ParameterUnsupported);
    }

    (body.contains("previous_response_id")
        || body.contains("previous response")
        || (body.contains("response") && body.contains("not found"))
        || (body.contains("response") && body.contains("expired")))
    .then_some(PreviousResponseStateRejection::ReferenceUnavailable)
}

#[async_trait::async_trait]
impl<C> ProviderSession for ResponsesSession<C>
where
    C: CredentialSource + 'static,
{
    async fn stream(&self, request: Request<'_>) -> Result<ProviderEventStream, ProviderError> {
        self.stream_response(request).await
    }

    async fn compact(
        &self,
        request: CompactionRequest<'_>,
    ) -> Result<CompactionResponse, ProviderError> {
        let request = request.into_model_request()?;
        let response = self.send_response(request).await?;
        Ok(response.into_compaction_response())
    }

    async fn summarize_memories(
        &self,
        request: MemorySummarizeRequest<'_>,
    ) -> Result<MemorySummarizeResponse, ProviderError> {
        let request = request.into_model_request()?;
        let response = self.send_response(request).await?;
        response.into_memory_summarize_response()
    }
}

#[cfg(test)]
mod tests {
    use std::borrow::Cow;
    use std::collections::BTreeMap;
    use std::io::Read;
    use std::io::Write;
    use std::net::TcpListener;
    use std::thread;

    use super::*;
    use crate::ProviderId;
    use crate::ProviderRequestOptions;
    use crate::StaticCredentialSource;
    use crate::responses::ResponsesProvider;

    fn spawn_single_response_server(
        response_body: &'static str,
    ) -> (String, thread::JoinHandle<String>) {
        spawn_single_response_server_with_headers(response_body, "")
    }

    fn spawn_single_response_server_with_headers(
        response_body: &'static str,
        extra_headers: &'static str,
    ) -> (String, thread::JoinHandle<String>) {
        let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
        let addr = listener.local_addr().expect("read listener addr");
        let handle = thread::spawn(move || {
            let (mut stream, _) = listener.accept().expect("accept request");
            let mut buffer = Vec::new();
            let mut temp = [0_u8; 1024];
            let mut header_end = None;
            let mut content_length = 0_usize;

            loop {
                let read = stream.read(&mut temp).expect("read request");
                if read == 0 {
                    break;
                }
                buffer.extend_from_slice(&temp[..read]);
                let discovered_header = if header_end.is_none() {
                    buffer.windows(4).position(|window| window == b"\r\n\r\n")
                } else {
                    None
                };
                if let Some(index) = discovered_header {
                    let end = index + 4;
                    header_end = Some(end);
                    let headers = String::from_utf8_lossy(&buffer[..end]);
                    content_length = headers
                        .lines()
                        .find_map(|line| {
                            let (name, value) = line.split_once(':')?;
                            name.eq_ignore_ascii_case("content-length").then(|| {
                                value.trim().parse::<usize>().expect("parse content-length")
                            })
                        })
                        .unwrap_or_default();
                }
                if header_end.is_some_and(|end| buffer.len() >= end + content_length) {
                    break;
                }
            }

            let response = format!(
                concat!(
                    "HTTP/1.1 200 OK\r\n",
                    "content-type: text/event-stream\r\n",
                    "{}",
                    "content-length: {}\r\n\r\n",
                    "{}"
                ),
                extra_headers,
                response_body.len(),
                response_body,
            );
            stream
                .write_all(response.as_bytes())
                .expect("write response");
            String::from_utf8(buffer).expect("request should be utf8")
        });

        (format!("http://{addr}/"), handle)
    }

    fn read_http_request(stream: &mut std::net::TcpStream) -> String {
        let mut buffer = Vec::new();
        let mut temp = [0_u8; 1024];
        let mut header_end = None;
        let mut content_length = 0_usize;

        loop {
            let read = stream.read(&mut temp).expect("read request");
            if read == 0 {
                break;
            }
            buffer.extend_from_slice(&temp[..read]);
            let discovered_header = if header_end.is_none() {
                buffer.windows(4).position(|window| window == b"\r\n\r\n")
            } else {
                None
            };
            if let Some(index) = discovered_header {
                let end = index + 4;
                header_end = Some(end);
                let headers = String::from_utf8_lossy(&buffer[..end]);
                content_length = headers
                    .lines()
                    .find_map(|line| {
                        let (name, value) = line.split_once(':')?;
                        name.eq_ignore_ascii_case("content-length")
                            .then(|| value.trim().parse::<usize>().expect("parse content-length"))
                    })
                    .unwrap_or_default();
            }
            if header_end.is_some_and(|end| buffer.len() >= end + content_length) {
                break;
            }
        }

        String::from_utf8(buffer).expect("request should be utf8")
    }

    fn request_body(captured: &str) -> &str {
        captured.split("\r\n\r\n").nth(1).unwrap_or_default()
    }

    fn spawn_hybrid_fallback_server(
        rejection_body: &'static str,
        successful_requests: usize,
    ) -> (String, thread::JoinHandle<Vec<String>>) {
        let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
        let addr = listener.local_addr().expect("read listener addr");
        let handle = thread::spawn(move || {
            let (mut first_stream, _) = listener.accept().expect("accept first request");
            let first = read_http_request(&mut first_stream);
            let first_response = format!(
                concat!(
                    "HTTP/1.1 400 Bad Request\r\n",
                    "connection: close\r\n",
                    "content-type: application/json\r\n",
                    "content-length: {}\r\n\r\n",
                    "{}"
                ),
                rejection_body.len(),
                rejection_body
            );
            first_stream
                .write_all(first_response.as_bytes())
                .expect("write first response");
            drop(first_stream);

            let mut captured = vec![first];
            for response_index in 1..=successful_requests {
                let (mut stream, _) = listener.accept().expect("accept successful request");
                captured.push(read_http_request(&mut stream));
                let response_id = format!("resp_fresh_{response_index}");
                let response_body = format!(
                    concat!(
                        "data: {{\"type\":\"response.created\",\"response\":{{\"id\":\"{}\",\"model\":\"gpt-5\",\"status\":\"in_progress\"}}}}\n\n",
                        "data: {{\"type\":\"response.completed\",\"response\":{{\"id\":\"{}\",\"model\":\"gpt-5\",\"status\":\"completed\"}}}}\n\n"
                    ),
                    response_id, response_id
                );
                let response = format!(
                    concat!(
                        "HTTP/1.1 200 OK\r\n",
                        "connection: close\r\n",
                        "content-type: text/event-stream\r\n",
                        "content-length: {}\r\n\r\n",
                        "{}"
                    ),
                    response_body.len(),
                    response_body
                );
                stream
                    .write_all(response.as_bytes())
                    .expect("write successful response");
            }

            captured
        });

        (format!("http://{addr}/"), handle)
    }

    async fn consume_stream(mut stream: ProviderEventStream) {
        while let Some(event) = stream.recv().await {
            event.expect("stream event should decode");
        }
    }

    fn spawn_two_turn_state_server() -> (String, thread::JoinHandle<(String, String)>) {
        let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
        let addr = listener.local_addr().expect("read listener addr");
        let handle = thread::spawn(move || {
            let (mut first_stream, _) = listener.accept().expect("accept first request");
            let first = read_http_request(&mut first_stream);
            let first_body = concat!(
                "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5\",\"status\":\"in_progress\"}}\n\n",
                "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5\",\"status\":\"completed\"}}\n\n"
            );
            let first_response = format!(
                concat!(
                    "HTTP/1.1 200 OK\r\n",
                    "connection: close\r\n",
                    "content-type: text/event-stream\r\n",
                    "x-codex-turn-state: state-1\r\n",
                    "content-length: {}\r\n\r\n",
                    "{}"
                ),
                first_body.len(),
                first_body
            );
            first_stream
                .write_all(first_response.as_bytes())
                .expect("write first response");
            drop(first_stream);

            let (mut second_stream, _) = listener.accept().expect("accept second request");
            let second = read_http_request(&mut second_stream);
            let second_body = concat!(
                "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_2\",\"model\":\"gpt-5\",\"status\":\"in_progress\"}}\n\n",
                "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_2\",\"model\":\"gpt-5\",\"status\":\"completed\"}}\n\n"
            );
            let second_response = format!(
                concat!(
                    "HTTP/1.1 200 OK\r\n",
                    "connection: close\r\n",
                    "content-type: text/event-stream\r\n",
                    "x-codex-turn-state: state-2\r\n",
                    "content-length: {}\r\n\r\n",
                    "{}"
                ),
                second_body.len(),
                second_body
            );
            second_stream
                .write_all(second_response.as_bytes())
                .expect("write second response");

            (first, second)
        });

        (format!("http://{addr}/"), handle)
    }

    fn spawn_compaction_response_server(
        response_body: &'static str,
    ) -> (String, thread::JoinHandle<(String, String)>) {
        let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
        let addr = listener.local_addr().expect("read listener addr");
        let handle = thread::spawn(move || {
            let (mut stream, _) = listener.accept().expect("accept request");
            let mut buffer = Vec::new();
            let mut temp = [0_u8; 1024];
            let mut header_end = None;
            let mut content_length = 0_usize;

            loop {
                let read = stream.read(&mut temp).expect("read request");
                if read == 0 {
                    break;
                }
                buffer.extend_from_slice(&temp[..read]);
                let discovered_header = if header_end.is_none() {
                    buffer.windows(4).position(|window| window == b"\r\n\r\n")
                } else {
                    None
                };
                if let Some(index) = discovered_header {
                    let end = index + 4;
                    header_end = Some(end);
                    let headers = String::from_utf8_lossy(&buffer[..end]);
                    content_length = headers
                        .lines()
                        .find_map(|line| {
                            let (name, value) = line.split_once(':')?;
                            name.eq_ignore_ascii_case("content-length").then(|| {
                                value.trim().parse::<usize>().expect("parse content-length")
                            })
                        })
                        .unwrap_or_default();
                }
                if header_end.is_some_and(|end| buffer.len() >= end + content_length) {
                    break;
                }
            }

            let response = format!(
                concat!(
                    "HTTP/1.1 200 OK\r\n",
                    "content-type: text/event-stream\r\n",
                    "content-length: {}\r\n\r\n",
                    "{}"
                ),
                response_body.len(),
                response_body
            );
            stream
                .write_all(response.as_bytes())
                .expect("write response");
            let captured = String::from_utf8(buffer).expect("request should be utf8");
            let body = captured
                .split("\r\n\r\n")
                .nth(1)
                .unwrap_or_default()
                .to_string();
            (captured, body)
        });

        (format!("http://{addr}/"), handle)
    }

    #[tokio::test]
    async fn stream_response_honors_session_request_options_on_http_path() {
        let sse_body = "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5\",\"status\":\"completed\"}}\n\n";
        let (base_url, handle) = spawn_single_response_server(sse_body);

        let mut definition = super::super::openai_definition();
        definition.base_url = Some(base_url);
        let session = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();
        session.set_turn_state("sticky-turn-state");

        let request = Request {
            model: Cow::Borrowed("gpt-5"),
            system: Some(Cow::Borrowed("system")),
            messages: Cow::Owned(vec![crate::Message::user(crate::ContentBlock::text(
                "hello",
            ))]),
            tools: Cow::Owned(Vec::new()),
            tool_choice: None,
            temperature: None,
            max_output_tokens: None,
            metadata: Cow::Owned(BTreeMap::new()),
            provider_request_options: ProviderRequestOptions {
                session: SessionRequestOptions {
                    sticky_turn_state: None,
                    turn_metadata: Some("{\"turn_id\":\"turn-123\"}".to_string()),
                    subagent: Some("memory_consolidation".to_string()),
                    prefer_connection_reuse: Some(true),
                    session_affinity: Some("session-affinity-123".to_string()),
                    extra_headers: BTreeMap::new(),
                },
                ..ProviderRequestOptions::default()
            },
        };

        let _stream = session
            .stream_response(request)
            .await
            .expect("stream response should succeed");

        let captured = handle.join().expect("server should capture request");
        assert!(captured.contains("x-codex-turn-state: sticky-turn-state\r\n"));
        assert!(captured.contains("x-codex-turn-metadata: {\"turn_id\":\"turn-123\"}\r\n"));
        assert!(captured.contains("x-mentra-turn-metadata: {\"turn_id\":\"turn-123\"}\r\n"));
        assert!(captured.contains("x-mentra-session-affinity: session-affinity-123\r\n"));
        assert!(captured.contains("x-mentra-connection-reuse: prefer-reuse\r\n"));
        assert!(captured.contains("x-client-request-id: session-affinity-123\r\n"));
        assert!(captured.contains("session_id: session-affinity-123\r\n"));
        assert!(captured.contains("x-openai-subagent: memory_consolidation\r\n"));
    }

    #[tokio::test]
    async fn live_http_path_threads_custom_provider_and_requested_model_provenance() {
        let sse_body = concat!(
            "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-resolved\",\"status\":\"in_progress\"}}\n\n",
            "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"reasoning\",\"id\":\"rs_new\",\"summary\":[]}}\n\n",
            "data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"type\":\"reasoning\",\"id\":\"rs_new\",\"encrypted_content\":\"encrypted-new\",\"summary\":[{\"type\":\"summary_text\",\"text\":\"new summary\"}]}}\n\n",
            "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-resolved\",\"status\":\"completed\"}}\n\n"
        );
        let (base_url, handle) = spawn_single_response_server(sse_body);
        let provider_id = ProviderId::new("openai-edge");
        let requested_model = "gpt-requested";
        let mut definition = super::super::openai_definition();
        definition.descriptor.id = provider_id.clone();
        definition.base_url = Some(base_url);
        let session = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();
        let request = Request {
            model: Cow::Borrowed(requested_model),
            system: None,
            messages: Cow::Owned(vec![crate::Message::assistant(
                crate::ContentBlock::Thinking {
                    thinking: "old summary".to_string(),
                    signature: None,
                    encrypted_content: Some("encrypted-old".to_string()),
                    id: Some("rs_old".to_string()),
                    provenance: Some(crate::ReasoningProvenance {
                        provider: provider_id.clone(),
                        model: requested_model.to_string(),
                        format: crate::ReasoningFormat::OpenAiEncrypted,
                    }),
                    redacted: false,
                },
            )]),
            tools: Cow::Owned(Vec::new()),
            tool_choice: None,
            temperature: None,
            max_output_tokens: None,
            metadata: Cow::Owned(BTreeMap::new()),
            provider_request_options: ProviderRequestOptions {
                reasoning: Some(crate::ReasoningOptions {
                    effort: Some(crate::ReasoningEffort::Medium),
                    summary: Some(crate::ReasoningSummary::Auto),
                }),
                ..ProviderRequestOptions::default()
            },
        };

        let response = session
            .send_response(request)
            .await
            .expect("custom Responses request should complete");
        let captured = handle.join().expect("server should capture request");
        let body: serde_json::Value =
            serde_json::from_str(request_body(&captured)).expect("request body should be JSON");

        assert_eq!(body["input"][0]["type"], "reasoning");
        assert_eq!(body["input"][0]["id"], "rs_old");
        assert_eq!(body["include"][0], "reasoning.encrypted_content");
        assert_eq!(
            response.content,
            vec![crate::ContentBlock::Thinking {
                thinking: "new summary".to_string(),
                signature: None,
                encrypted_content: Some("encrypted-new".to_string()),
                id: Some("rs_new".to_string()),
                provenance: Some(crate::ReasoningProvenance {
                    provider: provider_id,
                    model: requested_model.to_string(),
                    format: crate::ReasoningFormat::OpenAiEncrypted,
                }),
                redacted: false,
            }]
        );
    }

    #[tokio::test]
    async fn stream_response_captures_turn_state_from_http_response_headers() {
        let sse_body = "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5\",\"status\":\"completed\"}}\n\n";
        let (base_url, _handle) = spawn_single_response_server_with_headers(
            sse_body,
            "x-codex-turn-state: next-turn-state\r\n",
        );

        let mut definition = super::super::openai_definition();
        definition.base_url = Some(base_url);
        let session = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();

        let request = Request {
            model: Cow::Borrowed("gpt-5"),
            system: Some(Cow::Borrowed("system")),
            messages: Cow::Owned(vec![crate::Message::user(crate::ContentBlock::text(
                "hello",
            ))]),
            tools: Cow::Owned(Vec::new()),
            tool_choice: None,
            temperature: None,
            max_output_tokens: None,
            metadata: Cow::Owned(BTreeMap::new()),
            provider_request_options: ProviderRequestOptions::default(),
        };

        let _stream = session
            .stream_response(request)
            .await
            .expect("stream response should succeed");

        assert_eq!(session.turn_state().as_deref(), Some("next-turn-state"));
    }

    #[tokio::test]
    async fn http_turn_state_updates_across_multiple_turns() {
        let (base_url, handle) = spawn_two_turn_state_server();

        let mut definition = super::super::openai_definition();
        definition.base_url = Some(base_url);
        let session = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();

        for message in ["first", "second"] {
            let request = Request {
                model: Cow::Borrowed("gpt-5"),
                system: None,
                messages: Cow::Owned(vec![crate::Message::user(crate::ContentBlock::text(
                    message,
                ))]),
                tools: Cow::Owned(Vec::new()),
                tool_choice: None,
                temperature: None,
                max_output_tokens: None,
                metadata: Cow::Owned(BTreeMap::new()),
                provider_request_options: ProviderRequestOptions::default(),
            };

            let mut stream = session
                .stream_response(request)
                .await
                .expect("stream response should succeed");
            while let Some(event) = stream.recv().await {
                event.expect("stream event should decode");
            }
        }

        let (first, second) = handle.join().expect("server should capture requests");
        assert!(!first.contains("x-codex-turn-state:"));
        assert!(second.contains("x-codex-turn-state: state-1\r\n"));
        assert_eq!(session.turn_state().as_deref(), Some("state-2"));
        assert_eq!(session.latest_response_id().as_deref(), Some("resp_2"));
    }

    #[tokio::test]
    async fn predeclared_unsupported_endpoint_skips_the_first_hybrid_http_probe() {
        let (base_url, handle) = spawn_two_turn_state_server();

        let mut definition = super::super::openai_definition();
        definition.base_url = Some(base_url);
        let provider = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .without_hybrid_http_previous_response_id();

        for message in ["first", "second"] {
            let session = provider.session();
            let request = Request {
                model: Cow::Borrowed("gpt-5"),
                system: None,
                messages: Cow::Owned(vec![crate::Message::user(crate::ContentBlock::text(
                    message,
                ))]),
                tools: Cow::Owned(Vec::new()),
                tool_choice: None,
                temperature: None,
                max_output_tokens: None,
                metadata: Cow::Owned(BTreeMap::new()),
                provider_request_options: ProviderRequestOptions::default(),
            };

            consume_stream(
                session
                    .stream_response(request)
                    .await
                    .expect("hybrid replay should not probe unsupported provider state"),
            )
            .await;
        }

        let (first, second) = handle.join().expect("server should capture requests");
        let first_payload: serde_json::Value =
            serde_json::from_str(request_body(&first)).expect("first body should be json");
        let second_payload: serde_json::Value =
            serde_json::from_str(request_body(&second)).expect("second body should be json");
        assert!(first_payload.get("previous_response_id").is_none());
        assert!(second_payload.get("previous_response_id").is_none());
        assert_eq!(
            provider.session().latest_response_id().as_deref(),
            Some("resp_2")
        );
    }

    #[tokio::test]
    async fn stream_response_tracks_latest_response_id_from_http_events() {
        let sse_body = "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5\",\"status\":\"in_progress\"}}\n\n\
data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5\",\"status\":\"completed\"}}\n\n";
        let (base_url, _handle) = spawn_single_response_server(sse_body);

        let mut definition = super::super::openai_definition();
        definition.base_url = Some(base_url);
        let session = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();

        let request = Request {
            model: Cow::Borrowed("gpt-5"),
            system: None,
            messages: Cow::Owned(vec![crate::Message::user(crate::ContentBlock::text(
                "hello",
            ))]),
            tools: Cow::Owned(Vec::new()),
            tool_choice: None,
            temperature: None,
            max_output_tokens: None,
            metadata: Cow::Owned(BTreeMap::new()),
            provider_request_options: ProviderRequestOptions::default(),
        };

        let mut stream = session
            .stream_response(request)
            .await
            .expect("stream response should succeed");
        while let Some(event) = stream.recv().await {
            event.expect("stream event should decode");
        }

        assert_eq!(session.latest_response_id().as_deref(), Some("resp_1"));
    }

    #[test]
    fn responses_endpoint_path_supports_openai_and_xipe_base_urls() {
        assert_eq!(
            responses_endpoint_path_for_base(Some("https://api.openai.com/")),
            "v1/responses"
        );
        assert_eq!(
            responses_endpoint_path_for_base(Some("https://api.openai.com/v1")),
            "responses"
        );
        assert_eq!(
            responses_endpoint_path_for_base(Some("https://opencode.ai/zen/go/v1")),
            "responses"
        );
        assert_eq!(
            responses_endpoint_path_for_base(Some("https://chatgpt.com/backend-api/codex")),
            "responses"
        );
    }

    #[cfg(feature = "responses-websocket")]
    #[tokio::test]
    async fn websocket_transport_sends_response_create_frame() {
        use futures_util::{SinkExt, StreamExt};
        use http::HeaderValue;
        use tokio_tungstenite::accept_hdr_async;
        use tokio_tungstenite::tungstenite::Message as WsMessage;
        use tokio_tungstenite::tungstenite::handshake::server::{
            ErrorResponse, Request as WsHandshakeRequest, Response as WsHandshakeResponse,
        };

        #[expect(
            clippy::result_large_err,
            reason = "tokio-tungstenite fixes the handshake callback error type"
        )]
        fn add_turn_state_header(
            _request: &WsHandshakeRequest,
            mut response: WsHandshakeResponse,
        ) -> Result<WsHandshakeResponse, ErrorResponse> {
            response
                .headers_mut()
                .insert("x-codex-turn-state", HeaderValue::from_static("ws-state"));
            Ok(response)
        }

        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
            .await
            .expect("bind websocket test server");
        let addr = listener.local_addr().expect("read websocket server addr");
        let (tx_frame, rx_frame) = tokio::sync::oneshot::channel::<String>();

        tokio::spawn(async move {
            let (stream, _) = listener.accept().await.expect("accept websocket");
            let mut ws = accept_hdr_async(stream, add_turn_state_header)
                .await
                .expect("upgrade websocket");
            let frame = ws
                .next()
                .await
                .expect("client should send a frame")
                .expect("websocket frame should be valid")
                .into_text()
                .expect("request frame should be text")
                .to_string();
            tx_frame.send(frame).expect("send captured frame");

            ws.send(WsMessage::Text(
                serde_json::json!({
                    "type": "response.created",
                    "response": {
                        "id": "resp_ws",
                        "model": "gpt-resolved",
                        "status": "in_progress"
                    }
                })
                .to_string()
                .into(),
            ))
            .await
            .expect("send response.created");
            ws.send(WsMessage::Text(
                serde_json::json!({
                    "type": "response.output_item.added",
                    "output_index": 0,
                    "item": {
                        "type": "reasoning",
                        "id": "rs_ws",
                        "summary": []
                    }
                })
                .to_string()
                .into(),
            ))
            .await
            .expect("send reasoning start");
            ws.send(WsMessage::Text(
                serde_json::json!({
                    "type": "response.output_item.done",
                    "output_index": 0,
                    "item": {
                        "type": "reasoning",
                        "id": "rs_ws",
                        "encrypted_content": "encrypted-ws",
                        "summary": [{"type": "summary_text", "text": "ws summary"}]
                    }
                })
                .to_string()
                .into(),
            ))
            .await
            .expect("send reasoning completion");
            ws.send(WsMessage::Text(
                serde_json::json!({
                    "type": "response.completed",
                    "response": {
                        "id": "resp_ws",
                        "model": "gpt-resolved",
                        "status": "completed"
                    }
                })
                .to_string()
                .into(),
            ))
            .await
            .expect("send response.completed");
        });

        let mut definition = super::super::openai_definition();
        definition.descriptor.id = ProviderId::new("openai-ws-edge");
        definition.base_url = Some(format!("http://{addr}/v1"));
        let session = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();

        let request = Request {
            model: Cow::Borrowed("gpt-requested"),
            system: None,
            messages: Cow::Owned(vec![crate::Message::user(crate::ContentBlock::text(
                "hello",
            ))]),
            tools: Cow::Owned(Vec::new()),
            tool_choice: None,
            temperature: None,
            max_output_tokens: None,
            metadata: Cow::Owned(BTreeMap::new()),
            provider_request_options: ProviderRequestOptions {
                responses: crate::ResponsesRequestOptions {
                    transport: crate::ResponsesTransport::WebSocket,
                    ..Default::default()
                },
                ..ProviderRequestOptions::default()
            },
        };

        let mut stream = session
            .stream_response(request)
            .await
            .expect("websocket transport should stream");
        let mut events = Vec::new();
        while let Some(event) = stream.recv().await {
            events.push(event.expect("websocket event should parse"));
        }

        let frame: serde_json::Value =
            serde_json::from_str(&rx_frame.await.expect("server should capture frame"))
                .expect("frame should be json");
        assert_eq!(frame["type"], "response.create");
        assert_eq!(frame["model"], "gpt-requested");
        assert_eq!(frame["input"][0]["role"], "user");
        assert_eq!(frame["instructions"], "");
        assert!(frame.get("response").is_none());
        assert_eq!(session.turn_state().as_deref(), Some("ws-state"));
        assert_eq!(session.latest_response_id().as_deref(), Some("resp_ws"));
        assert!(events.iter().any(|event| matches!(
            event,
            ProviderEvent::ContentBlockStarted {
                kind: crate::ContentBlockStart::Thinking {
                    id: Some(id),
                    provenance: Some(crate::ReasoningProvenance {
                        provider,
                        model,
                        format: crate::ReasoningFormat::OpenAiEncrypted,
                    }),
                    ..
                },
                ..
            } if id == "rs_ws"
                && provider == &ProviderId::new("openai-ws-edge")
                && model == "gpt-requested"
        )));
    }

    #[tokio::test]
    async fn hybrid_state_falls_back_to_replay_when_previous_response_id_is_rejected() {
        let (base_url, handle) = spawn_hybrid_fallback_server(
            r#"{"error":{"message":"previous_response_id not found"}}"#,
            2,
        );

        let mut definition = super::super::openai_definition();
        definition.base_url = Some(base_url);
        let session = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();
        session.state.set_latest_response_id("resp_stale");

        let request = Request {
            model: Cow::Borrowed("gpt-5"),
            system: None,
            messages: Cow::Owned(vec![crate::Message::user(crate::ContentBlock::text(
                "hello",
            ))]),
            tools: Cow::Owned(Vec::new()),
            tool_choice: None,
            temperature: None,
            max_output_tokens: None,
            metadata: Cow::Owned(BTreeMap::new()),
            provider_request_options: ProviderRequestOptions::default(),
        };

        let stream = session
            .stream_response(request.clone())
            .await
            .expect("hybrid fallback should retry without provider state");
        consume_stream(stream).await;

        let stream = session
            .stream_response(request)
            .await
            .expect("fresh provider state should remain usable");
        consume_stream(stream).await;

        let captured = handle.join().expect("server should capture requests");
        let first_payload: serde_json::Value =
            serde_json::from_str(request_body(&captured[0])).expect("first body should be json");
        let second_payload: serde_json::Value =
            serde_json::from_str(request_body(&captured[1])).expect("second body should be json");
        let third_payload: serde_json::Value =
            serde_json::from_str(request_body(&captured[2])).expect("third body should be json");
        assert_eq!(first_payload["previous_response_id"], "resp_stale");
        assert!(second_payload.get("previous_response_id").is_none());
        assert_eq!(third_payload["previous_response_id"], "resp_fresh_1");
        assert_eq!(
            session.latest_response_id().as_deref(),
            Some("resp_fresh_2")
        );
    }

    #[test]
    fn classifies_previous_response_state_rejections_conservatively() {
        let unsupported = ProviderError::Http {
            status: reqwest::StatusCode::BAD_REQUEST,
            body: r#"{"detail":"Unsupported parameter: previous_response_id"}"#.to_string(),
            retry_after: None,
        };
        assert_eq!(
            previous_response_state_rejection(&unsupported),
            Some(PreviousResponseStateRejection::ParameterUnsupported)
        );

        let stale = ProviderError::Http {
            status: reqwest::StatusCode::NOT_FOUND,
            body: r#"{"error":{"message":"previous_response_id expired"}}"#.to_string(),
            retry_after: None,
        };
        assert_eq!(
            previous_response_state_rejection(&stale),
            Some(PreviousResponseStateRejection::ReferenceUnavailable)
        );

        let unrelated = ProviderError::Http {
            status: reqwest::StatusCode::BAD_REQUEST,
            body: r#"{"detail":"Unsupported parameter: temperature"}"#.to_string(),
            retry_after: None,
        };
        assert_eq!(previous_response_state_rejection(&unrelated), None);

        let echoed_previous_response_id = ProviderError::Http {
            status: reqwest::StatusCode::BAD_REQUEST,
            body: r#"{"detail":"Unsupported parameter: temperature","request":{
                    "previous_response_id":"resp_1"}}"#
                .to_string(),
            retry_after: None,
        };
        assert_eq!(
            previous_response_state_rejection(&echoed_previous_response_id),
            Some(PreviousResponseStateRejection::ReferenceUnavailable)
        );
    }

    #[tokio::test]
    async fn hybrid_state_remembers_when_previous_response_id_is_unsupported() {
        let (base_url, handle) = spawn_hybrid_fallback_server(
            r#"{"detail":"Unsupported parameter: previous_response_id"}"#,
            4,
        );

        let mut definition = super::super::openai_definition();
        definition.base_url = Some(base_url);
        let provider = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        );
        let session = provider.session();
        session.state.set_latest_response_id("resp_stale");

        let request = Request {
            model: Cow::Borrowed("gpt-5"),
            system: None,
            messages: Cow::Owned(vec![crate::Message::user(crate::ContentBlock::text(
                "hello",
            ))]),
            tools: Cow::Owned(Vec::new()),
            tool_choice: None,
            temperature: None,
            max_output_tokens: None,
            metadata: Cow::Owned(BTreeMap::new()),
            provider_request_options: ProviderRequestOptions::default(),
        };

        let stream = session
            .stream_response(request.clone())
            .await
            .expect("hybrid fallback should retry without unsupported provider state");
        consume_stream(stream).await;

        let next_session = provider.session();
        let stream = next_session
            .stream_response(request.clone())
            .await
            .expect("same-model replay should skip unsupported provider state");
        consume_stream(stream).await;

        let mut other_model_request = request.clone();
        other_model_request.model = Cow::Borrowed("gpt-5-mini");
        let stream = next_session
            .stream_response(other_model_request)
            .await
            .expect("another model should keep opportunistic provider state");
        consume_stream(stream).await;

        let mut stateful_request = request;
        stateful_request
            .provider_request_options
            .responses
            .state_mode = crate::ResponsesStateMode::Stateful;
        let stream = next_session
            .stream_response(stateful_request)
            .await
            .expect("stateful mode should ignore the hybrid capability cache");
        consume_stream(stream).await;

        let captured = handle.join().expect("server should capture requests");
        let payloads = captured
            .iter()
            .map(|request| {
                serde_json::from_str::<serde_json::Value>(request_body(request))
                    .expect("request body should be json")
            })
            .collect::<Vec<_>>();
        assert_eq!(payloads[0]["previous_response_id"], "resp_stale");
        assert!(payloads[1].get("previous_response_id").is_none());
        assert!(payloads[2].get("previous_response_id").is_none());
        assert_eq!(payloads[3]["previous_response_id"], "resp_fresh_2");
        assert_eq!(payloads[4]["previous_response_id"], "resp_fresh_3");
        assert_eq!(
            next_session.latest_response_id().as_deref(),
            Some("resp_fresh_4")
        );
    }

    #[tokio::test]
    async fn compact_sends_normal_model_request_and_wraps_summary_text() {
        let sse_body = concat!(
            "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5\",\"status\":\"in_progress\"}}\n\n",
            "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"content\":[]}}\n\n",
            "data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"{\\\"goal\\\":\\\"keep going\\\"}\"}]}}\n\n",
            "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5\",\"status\":\"completed\"}}\n\n"
        );
        let (base_url, handle) = spawn_compaction_response_server(sse_body);

        let mut definition = super::super::openai_definition();
        definition.base_url = Some(base_url);
        let session = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();

        let request = crate::CompactionRequest {
            model: Cow::Borrowed("gpt-5"),
            instructions: Cow::Borrowed("Summarize the transcript."),
            input: Cow::Owned(vec![crate::CompactionInputItem::UserTurn {
                content: "hello".to_string(),
            }]),
            metadata: Cow::Owned(BTreeMap::from([("scope".to_string(), "test".to_string())])),
            provider_request_options: crate::ProviderRequestOptions::default(),
        };

        let response = session.compact(request).await.expect("compaction succeeds");
        let captured = handle.join().expect("server should capture request");

        let payload: serde_json::Value =
            serde_json::from_str(&captured.1).expect("request body should be json");
        assert_eq!(payload["model"], "gpt-5");
        assert_eq!(payload["instructions"], "Summarize the transcript.");
        assert_eq!(payload["metadata"]["scope"], "test");
        assert_eq!(payload["input"][0]["content"][0]["type"], "input_text");
        assert!(
            payload["input"][0]["content"][0]["text"]
                .as_str()
                .expect("prompt text should be a string")
                .starts_with("Compaction input JSON:\n")
        );

        assert_eq!(response.output.len(), 1);
        assert_eq!(
            response.output[0],
            crate::CompactionInputItem::CompactionSummary {
                content: "{\"goal\":\"keep going\"}".to_string()
            }
        );
    }

    #[tokio::test]
    async fn summarize_memories_sends_normal_model_request_and_parses_json_output() {
        let sse_body = concat!(
            "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_2\",\"model\":\"gpt-5\",\"status\":\"in_progress\"}}\n\n",
            "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"content\":[]}}\n\n",
            "data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"[{\\\"raw_memory\\\":\\\"Detailed summary\\\",\\\"memory_summary\\\":\\\"Short summary\\\"}]\"}]}}\n\n",
            "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_2\",\"model\":\"gpt-5\",\"status\":\"completed\"}}\n\n"
        );
        let (base_url, handle) = spawn_compaction_response_server(sse_body);

        let mut definition = super::super::openai_definition();
        definition.base_url = Some(base_url);
        let session = ResponsesProvider::with_shared_credential_source(
            definition,
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();

        let request = crate::MemorySummarizeRequest {
            model: Cow::Borrowed("gpt-5"),
            raw_memories: Cow::Owned(vec![crate::RawMemory {
                id: "memory-1".to_string(),
                metadata: crate::RawMemoryMetadata {
                    source_path: "/tmp/trace.jsonl".to_string(),
                },
                items: vec![serde_json::json!({"type":"message","role":"user"})],
            }]),
            reasoning: Some(crate::ReasoningOptions {
                effort: Some(crate::ReasoningEffort::Medium),
                summary: None,
            }),
            metadata: Cow::Owned(BTreeMap::from([("scope".to_string(), "test".to_string())])),
            provider_request_options: crate::ProviderRequestOptions {
                session: crate::SessionRequestOptions {
                    sticky_turn_state: None,
                    turn_metadata: Some("{\"turn_id\":\"turn-321\"}".to_string()),
                    subagent: Some("compact".to_string()),
                    prefer_connection_reuse: Some(true),
                    session_affinity: Some("session-affinity-321".to_string()),
                    extra_headers: BTreeMap::new(),
                },
                ..crate::ProviderRequestOptions::default()
            },
        };

        let response = session
            .summarize_memories(request)
            .await
            .expect("memory summarization succeeds");
        let captured = handle.join().expect("server should capture request");

        let payload: serde_json::Value =
            serde_json::from_str(&captured.1).expect("request body should be json");
        assert_eq!(payload["model"], "gpt-5");
        assert_eq!(payload["reasoning"]["effort"], "medium");
        assert_eq!(payload["metadata"]["scope"], "test");
        assert_eq!(payload["input"][0]["content"][0]["type"], "input_text");
        assert!(
            payload["input"][0]["content"][0]["text"]
                .as_str()
                .expect("prompt text should be a string")
                .starts_with("Memory summarize input JSON:\n")
        );

        assert_eq!(response.output.len(), 1);
        assert_eq!(response.output[0].raw_memory, "Detailed summary");
        assert_eq!(response.output[0].memory_summary, "Short summary");
        assert!(
            captured
                .0
                .contains("x-codex-turn-metadata: {\"turn_id\":\"turn-321\"}\r\n")
        );
        assert!(
            captured
                .0
                .contains("x-mentra-session-affinity: session-affinity-321\r\n")
        );
        assert!(
            captured
                .0
                .contains("x-client-request-id: session-affinity-321\r\n")
        );
        assert!(captured.0.contains("session_id: session-affinity-321\r\n"));
        assert!(captured.0.contains("x-openai-subagent: compact\r\n"));
    }

    #[cfg(feature = "responses-websocket")]
    #[tokio::test]
    async fn websocket_connection_is_closed_without_cached_connection() {
        let session = ResponsesProvider::with_shared_credential_source(
            super::super::openai_definition(),
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();

        assert!(session.websocket_connection_is_closed().await);
        session.clear_websocket_connection();
        assert!(session.websocket_connection_is_closed().await);
    }

    /// A build without the transport must say so, and name the way back.
    ///
    /// The failure has to be visible at the call that asked for it: no panic,
    /// and no silent demotion to HTTP+SSE, either of which would let a build
    /// that cannot do what was configured pass for one that can. `openai` is
    /// the preset whose capabilities advertise websockets, so this reaches the
    /// missing transport rather than the runtime `websockets_enabled` check.
    #[cfg(not(feature = "responses-websocket"))]
    #[tokio::test]
    async fn websocket_transport_without_the_feature_is_an_honest_error() {
        let session = ResponsesProvider::with_shared_credential_source(
            super::super::openai_definition(),
            Arc::new(StaticCredentialSource::new("test-key")),
        )
        .session();

        let request = Request {
            model: Cow::Borrowed("gpt-requested"),
            system: None,
            messages: Cow::Owned(vec![crate::Message::user(crate::ContentBlock::text(
                "hello",
            ))]),
            tools: Cow::Owned(Vec::new()),
            tool_choice: None,
            temperature: None,
            max_output_tokens: None,
            metadata: Cow::Owned(BTreeMap::new()),
            provider_request_options: ProviderRequestOptions {
                responses: crate::ResponsesRequestOptions {
                    transport: crate::ResponsesTransport::WebSocket,
                    ..Default::default()
                },
                ..ProviderRequestOptions::default()
            },
        };

        let error = session
            .stream_response(request)
            .await
            .err()
            .expect("a transport that was not compiled in cannot stream");

        assert!(
            matches!(error, ProviderError::UnsupportedCapability(_)),
            "expected an unsupported-capability error, got {error:?}"
        );
        assert_eq!(
            error.to_string(),
            "provider does not support capability: responses_websocket (not compiled in: \
             rebuild mentra-provider with the `responses-websocket` feature)",
            "the message is the only thing that tells an operator how to fix the build"
        );
    }
}
