//! IDP HTTP client trait and implementation.

// WASM is single-threaded, so futures don't need to be Send
#![cfg_attr(target_arch = "wasm32", allow(clippy::future_not_send))]

use async_trait::async_trait;
use uuid::Uuid;

use crate::config::{NetworkConfig, RetryPolicy};
use crate::constants::DEFAULT_RETRY_AFTER_SECONDS;
use crate::error::HttpError;
use crate::http::{HttpClient, execute_with_retry};
use crate::types::{CredentialPolicyRequest, CredentialRequestResponse, RequestId, RequestStatus};
use crate::verification::VerifiablePresentation;

/// HTTP client interface for IDP operations.
///
/// This trait abstracts HTTP communication with the `LastID` IDP, enabling:
/// 1. Testing with mock implementations
/// 2. Platform-specific HTTP clients (native reqwest vs wasm fetch)
/// 3. Retry and error handling strategies
// Native: requires Send + Sync for thread safety
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
pub trait IdpClient: Send + Sync {
    /// Request a credential presentation from the IDP.
    ///
    /// Returns the full response including the `openid4vp://` request URI
    /// needed for QR code display.
    ///
    /// # Arguments
    ///
    /// * `policy` - Credential policy request
    /// * `bearer_token` - OAuth DPoP-bound access token
    /// * `dpop_proof` - `DPoP` proof JWT with ath claim for token binding
    async fn request_credential(
        &self,
        policy: CredentialPolicyRequest,
        bearer_token: &str,
        dpop_proof: &str,
    ) -> Result<CredentialRequestResponse, HttpError>;

    /// Poll for credential request status.
    ///
    /// # Arguments
    ///
    /// * `request_id` - Request identifier from `request_credential`
    /// * `client_id` - OAuth client ID for CORS preflight validation
    async fn poll_status(
        &self,
        request_id: &RequestId,
        client_id: &str,
    ) -> Result<RequestStatus, HttpError>;

    /// Retrieve the presentation for a fulfilled request.
    ///
    /// # Arguments
    ///
    /// * `request_id` - Request identifier
    /// * `client_id` - OAuth client ID for CORS preflight validation
    async fn get_presentation(
        &self,
        request_id: &RequestId,
        client_id: &str,
    ) -> Result<VerifiablePresentation, HttpError>;
}

// WASM: single-threaded, no Send + Sync needed
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
pub trait IdpClient {
    /// Request a credential presentation from the IDP.
    ///
    /// Returns the full response including the `openid4vp://` request URI
    /// needed for QR code display.
    ///
    /// # Arguments
    ///
    /// * `policy` - Credential policy request
    /// * `bearer_token` - OAuth DPoP-bound access token
    /// * `dpop_proof` - `DPoP` proof JWT with ath claim for token binding
    ///
    /// # Errors
    ///
    /// * `HttpError::Network` - Network failure (retryable)
    /// * `HttpError::Status { 400 }` - Invalid policy (non-retryable)
    /// * `HttpError::Status { 401 }` - Invalid token (non-retryable)
    /// * `HttpError::RateLimited` - Rate limit exceeded (retryable with
    ///   backoff)
    async fn request_credential(
        &self,
        policy: CredentialPolicyRequest,
        bearer_token: &str,
        dpop_proof: &str,
    ) -> Result<CredentialRequestResponse, HttpError>;

    /// Poll for credential request status.
    ///
    /// # Arguments
    ///
    /// * `request_id` - Request identifier from `request_credential`
    /// * `client_id` - OAuth client ID for CORS preflight validation
    ///
    /// # Errors
    ///
    /// * `HttpError::Status { 404 }` - Request ID not found
    /// * `HttpError::Network` - Network failure (retryable)
    async fn poll_status(
        &self,
        request_id: &RequestId,
        client_id: &str,
    ) -> Result<RequestStatus, HttpError>;

    /// Retrieve the presentation for a fulfilled request.
    ///
    /// # Arguments
    ///
    /// * `request_id` - Request identifier
    /// * `client_id` - OAuth client ID for CORS preflight validation
    ///
    /// # Errors
    ///
    /// * `HttpError::Status { 404 }` - Request not fulfilled or not found
    /// * `HttpError::Status { 410 }` - Presentation already consumed
    async fn get_presentation(
        &self,
        request_id: &RequestId,
        client_id: &str,
    ) -> Result<VerifiablePresentation, HttpError>;
}

/// Correlation ID configuration for request tracing.
#[derive(Debug, Clone)]
pub struct CorrelationIdConfig {
    /// Whether correlation IDs are enabled.
    pub enabled: bool,
    /// The header name to use for correlation IDs.
    pub header_name: String,
}

impl Default for CorrelationIdConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            header_name: crate::constants::DEFAULT_CORRELATION_ID_HEADER.to_string(),
        }
    }
}

/// Production HTTP client for IDP operations.
pub struct HttpIdpClient {
    http_client: HttpClient,
    base_url: String,
    retry_policy: RetryPolicy,
    correlation_config: CorrelationIdConfig,
}

impl HttpIdpClient {
    /// Create a new IDP client with a simple timeout.
    ///
    /// This is a convenience constructor that creates a client with default
    /// network configuration except for the request timeout. For full control
    /// over proxy and timeout settings, use
    /// [`HttpIdpClient::with_network_config`].
    ///
    /// # Arguments
    ///
    /// * `base_url` - IDP base URL (e.g., `https://human.lastid.co`)
    /// * `timeout_seconds` - HTTP request timeout
    /// * `retry_policy` - Retry configuration
    ///
    /// # Errors
    ///
    /// Returns `HttpError::Network` if HTTP client creation fails.
    pub fn new(
        base_url: String,
        timeout_seconds: u64,
        retry_policy: RetryPolicy,
    ) -> Result<Self, HttpError> {
        let network_config = NetworkConfig {
            request_timeout_seconds: timeout_seconds,
            ..Default::default()
        };
        Self::with_network_config(base_url, &network_config, retry_policy)
    }

    /// Create a new IDP client with full network configuration.
    ///
    /// This constructor provides full control over HTTP client settings
    /// including:
    /// - Proxy configuration (HTTP/HTTPS proxies, `no_proxy` exclusions)
    /// - Granular timeout settings (connect, read, request)
    /// - Connection pool tuning
    /// - Correlation ID settings
    ///
    /// # Arguments
    ///
    /// * `base_url` - IDP base URL (e.g., `https://human.lastid.co`)
    /// * `network_config` - Full network configuration
    /// * `retry_policy` - Retry configuration
    ///
    /// # Errors
    ///
    /// Returns `HttpError::Network` if HTTP client creation fails (e.g.,
    /// invalid proxy URL).
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::{HttpIdpClient, NetworkConfig, RetryPolicy};
    ///
    /// let network_config = NetworkConfig {
    ///     proxy_url: Some("http://proxy.corp.example.com:8080".to_string()),
    ///     connect_timeout_seconds: 5,
    ///     read_timeout_seconds: 30,
    ///     request_timeout_seconds: 60,
    ///     ..Default::default()
    /// };
    ///
    /// let client = HttpIdpClient::with_network_config(
    ///     "https://human.lastid.co".to_string(),
    ///     &network_config,
    ///     RetryPolicy::default(),
    /// )?;
    /// # Ok::<(), lastid_sdk::HttpError>(())
    /// ```
    pub fn with_network_config(
        base_url: String,
        network_config: &NetworkConfig,
        retry_policy: RetryPolicy,
    ) -> Result<Self, HttpError> {
        let correlation_config = CorrelationIdConfig {
            enabled: network_config.enable_correlation_ids,
            header_name: network_config.correlation_id_header.clone(),
        };

        let http_client = HttpClient::new(network_config)?;

        Ok(Self {
            http_client,
            base_url,
            retry_policy,
            correlation_config,
        })
    }

    /// Generate a new correlation ID for request tracing.
    ///
    /// Returns `None` if correlation IDs are disabled.
    fn generate_correlation_id(&self) -> Option<String> {
        if self.correlation_config.enabled {
            Some(Uuid::new_v4().to_string())
        } else {
            None
        }
    }

    /// Parse HTTP response into an error if not successful.
    async fn response_to_error(response: reqwest::Response) -> HttpError {
        let status = response.status().as_u16();

        // Check for rate limiting
        if status == 429 {
            let retry_after = response
                .headers()
                .get("Retry-After")
                .and_then(|v| v.to_str().ok())
                .and_then(|s| s.parse().ok())
                .unwrap_or(DEFAULT_RETRY_AFTER_SECONDS);

            return HttpError::rate_limited(retry_after);
        }

        let message = response.text().await.unwrap_or_default();
        HttpError::status(status, message)
    }
}

/// Check if a JWT part is an issuer-signed SD-JWT (typ: vc+sd-jwt).
///
/// Returns true if the JWT header contains `"typ": "vc+sd-jwt"`, which indicates
/// it's an issuer-signed credential (starts a new SD-JWT).
/// Returns false for KB-JWTs (`kb+jwt`) or non-JWTs.
fn is_issuer_jwt(part: &str) -> bool {
    use base64::Engine;
    use base64::engine::general_purpose::URL_SAFE_NO_PAD;

    // Must look like a JWT (has dots)
    if !part.contains('.') {
        return false;
    }

    // Extract and decode the header (first part before .)
    let header_b64 = match part.split('.').next() {
        Some(h) if h.starts_with("eyJ") => h,
        _ => return false,
    };

    // Decode header and check typ
    if let Ok(header_bytes) = URL_SAFE_NO_PAD.decode(header_b64)
        && let Ok(header) = serde_json::from_slice::<serde_json::Value>(&header_bytes)
    {
        return header.get("typ").and_then(|v| v.as_str()) == Some("vc+sd-jwt");
    }

    false
}

/// Split VP tokens that are joined by `~`.
///
/// VP tokens from IDP are joined with `~`, but `~` is also used within SD-JWTs
/// as the disclosure separator. This function correctly splits them by detecting
/// issuer-signed JWTs (typ: vc+sd-jwt) which start new credentials.
///
/// # SD-JWT Structure
///
/// Each SD-JWT has the format: `<issuer-jwt>~<disclosure1>~...~<kb-jwt>`
/// - Issuer JWT has `typ: "vc+sd-jwt"` in header
/// - KB-JWT has `typ: "kb+jwt"` in header
/// - Disclosures are base64url encoded arrays (not JWTs)
fn split_vp_tokens(presentation: &str) -> Vec<String> {
    if presentation.is_empty() {
        return Vec::new();
    }

    let parts: Vec<&str> = presentation.split('~').collect();
    let mut tokens: Vec<String> = Vec::new();
    let mut current_token = String::new();

    for part in parts {
        // Check if this part is an issuer-signed JWT (starts a new credential)
        if is_issuer_jwt(part) && !current_token.is_empty() {
            // This is a new credential's issuer JWT, save current and start fresh
            tokens.push(current_token);
            current_token = part.to_string();
        } else {
            // Part of current credential (disclosure, KB-JWT, or first issuer JWT)
            if !current_token.is_empty() {
                current_token.push('~');
            }
            current_token.push_str(part);
        }
    }

    if !current_token.is_empty() {
        tokens.push(current_token);
    }

    tokens
}

/// Build IDP verification request from SDK policy.
///
/// Transforms the SDK's `CredentialPolicyRequest` into the IDP's expected
/// `VerificationRequestInput` format following `OpenID4VP` specification.
///
/// # Presentation Definition Structure
///
/// Creates a SINGLE `input_descriptor` with the credential chain in an `enum`
/// filter. This matches the human-idp-client reference implementation and allows
/// the wallet to present any credential from the chain.
///
/// # Field Handling
///
/// - **Required fields** (`required_fields`): Each field becomes a path entry
///   WITHOUT `optional: true` - the field MUST be disclosed.
/// - **Optional fields** (`optional_fields`): Each field becomes a path entry
///   WITH `optional: true` - user can choose whether to disclose.
///
/// # Example
///
/// For `Persona` with required `display_name` and optional `email`:
/// ```json
/// {
///   "input_descriptors": [{
///     "id": "credential",
///     "constraints": {
///       "fields": [
///         { "path": ["$.vct"], "filter": { "type": "string", "enum": [...] } },
///         { "path": ["$.display_name"] },
///         { "path": ["$.email"], "optional": true }
///       ]
///     }
///   }]
/// }
/// ```
fn build_verification_request(policy: &CredentialPolicyRequest) -> serde_json::Value {
    use serde_json::json;

    // Use credential_types (the chain) for the vct filter
    let credential_types = &policy.credential_types;

    // Build vct filter - use const when single type, enum when multiple types
    // This matches the human-idp-client reference implementation
    let vct_filter = if credential_types.len() == 1 {
        json!({
            "type": "string",
            "const": credential_types[0]
        })
    } else {
        json!({
            "type": "string",
            "enum": credential_types
        })
    };

    // Build fields array starting with vct constraint
    let mut fields = vec![json!({
        "path": ["$.vct"],
        "filter": vct_filter
    })];

    // Add required fields (no optional flag = must be present)
    if let Some(required) = &policy.required_fields {
        for field in required {
            fields.push(json!({
                "path": [format!("$.{}", field)]
            }));
        }
    }

    // Add optional fields (user can choose to disclose)
    if let Some(optional) = &policy.optional_fields {
        for field in optional {
            fields.push(json!({
                "path": [format!("$.{}", field)],
                "optional": true
            }));
        }
    }

    // Add other constraint fields (domain, issuer, verification level, etc.)
    if let Some(constraints) = &policy.constraints {
        for (key, value) in constraints {
            // Skip deprecated requestedFields/optionalFields keys
            if key == "requestedFields" || key == "optionalFields" {
                continue;
            }
            fields.push(json!({
                "path": [format!("$.{}", key)],
                "filter": value
            }));
        }
    }

    let mut request = json!({
        "client_id": &policy.client_id,
        "response_type": "vp_token",
        "response_mode": "direct_post",
        "presentation_definition": {
            "id": "sdk-request",
            "input_descriptors": [{
                "id": "credential",
                "constraints": {
                    "fields": fields
                }
            }]
        }
    });

    // Add OpenID4VP top-level fields (NOT credential constraints)
    // These are standard OAuth/OpenID4VP request parameters

    // redirect_uri: where to redirect after completion
    if let Some(callback) = &policy.callback_url {
        request["redirect_uri"] = json!(callback);
    }

    // state: CSRF protection token, returned unchanged in response
    if let Some(state) = &policy.state {
        request["state"] = json!(state);
    }

    // nonce: cryptographic binding value for key binding proofs
    if let Some(nonce) = &policy.nonce {
        request["nonce"] = json!(nonce);
    }

    request
}

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl IdpClient for HttpIdpClient {
    async fn request_credential(
        &self,
        policy: CredentialPolicyRequest,
        bearer_token: &str,
        dpop_proof: &str,
    ) -> Result<CredentialRequestResponse, HttpError> {
        // OpenID4VP verification request endpoint
        // Include client_id in query string for CORS preflight validation.
        // Browser preflight (OPTIONS) requests have no body, so the IDP needs
        // client_id in the URL to validate the origin against allowedOrigins.
        let url = format!(
            "{}/v1/verify/request?client_id={}",
            self.base_url,
            urlencoding::encode(&policy.client_id)
        );
        let token = bearer_token.to_string();
        let proof = dpop_proof.to_string();
        let policy_clone = policy.clone();

        // Generate correlation ID once per request (reused across retry attempts)
        let correlation_id = self.generate_correlation_id();
        let correlation_header = self.correlation_config.header_name.clone();

        #[cfg(feature = "tracing")]
        if let Some(ref cid) = correlation_id {
            tracing::debug!(correlation_id = %cid, "Generated correlation ID for credential request");
        }

        execute_with_retry(&self.retry_policy, || {
            let url = url.clone();
            let token = token.clone();
            let proof = proof.clone();
            let policy = policy_clone.clone();
            let client = &self.http_client;
            let correlation_id = correlation_id.clone();
            let correlation_header = correlation_header.clone();

            async move {
                // Transform SDK policy to IDP's VerificationRequestInput format
                let idp_request = build_verification_request(&policy);

                // DPoP-bound tokens use DPoP scheme, not Bearer (RFC 9449)
                let mut request_builder = client
                    .post(&url)
                    .header("Authorization", format!("DPoP {token}"))
                    .header("DPoP", &proof);

                // Add correlation ID header if enabled
                if let Some(ref cid) = correlation_id {
                    request_builder = request_builder.header(&correlation_header, cid);
                }

                let response = request_builder
                    .json(&idp_request)
                    .send()
                    .await
                    .map_err(|e| {
                        if e.is_timeout() {
                            HttpError::Timeout
                        } else {
                            HttpError::network(e.to_string())
                        }
                    })?;

                if response.status().is_success() {
                    // IDP returns: { request_id, request_uri, expires_in, nonce }
                    #[derive(serde::Deserialize)]
                    struct IdpResponse {
                        request_id: String,
                        request_uri: String,
                        expires_in: u64,
                        nonce: String,
                    }

                    let body: IdpResponse = response
                        .json()
                        .await
                        .map_err(|e| HttpError::network(e.to_string()))?;

                    // Validate OpenID4VP URI scheme
                    if !body.request_uri.starts_with("openid4vp://")
                        && !body.request_uri.starts_with("openid://")
                    {
                        return Err(HttpError::network(format!(
                            "Invalid request_uri scheme: expected openid4vp:// or openid://, got {}",
                            body.request_uri.split("://").next().unwrap_or("unknown")
                        )));
                    }

                    Ok(CredentialRequestResponse::new(
                        RequestId::new(body.request_id),
                        body.request_uri,
                        body.expires_in,
                        body.nonce,
                    ))
                } else {
                    Err(Self::response_to_error(response).await)
                }
            }
        })
        .await
    }

    async fn poll_status(
        &self,
        request_id: &RequestId,
        client_id: &str,
    ) -> Result<RequestStatus, HttpError> {
        // OpenID4VP status endpoint
        // Include client_id in query string for CORS preflight validation.
        // Browser preflight (OPTIONS) requests have no body, so the IDP needs
        // client_id in the URL to validate the origin against allowedOrigins.
        let url = format!(
            "{}/v1/verify/status/{}?client_id={}",
            self.base_url,
            request_id.as_str(),
            urlencoding::encode(client_id)
        );
        let rid = request_id.clone();

        // Generate correlation ID once per request (reused across retry attempts)
        let correlation_id = self.generate_correlation_id();
        let correlation_header = self.correlation_config.header_name.clone();

        #[cfg(feature = "tracing")]
        if let Some(ref cid) = correlation_id {
            tracing::debug!(correlation_id = %cid, request_id = %request_id.as_str(), "Generated correlation ID for status poll");
        }

        execute_with_retry(&self.retry_policy, || {
            let url = url.clone();
            let client = &self.http_client;
            let request_id = rid.clone();
            let correlation_id = correlation_id.clone();
            let correlation_header = correlation_header.clone();

            async move {
                let mut request_builder = client.get(&url);

                // Add correlation ID header if enabled
                if let Some(ref cid) = correlation_id {
                    request_builder = request_builder.header(&correlation_header, cid);
                }

                let response = request_builder.send().await.map_err(|e| {
                    if e.is_timeout() {
                        HttpError::Timeout
                    } else {
                        HttpError::network(e.to_string())
                    }
                })?;

                if response.status().is_success() {
                    // IDP returns: { request_id, state, result?, expires_at, vp_token? }
                    // vp_token is an array of SD-JWT strings (one per credential)
                    #[derive(serde::Deserialize)]
                    #[allow(dead_code)]
                    struct IdpStatus {
                        request_id: String,
                        state: String,
                        /// VP tokens - array of SD-JWT strings per `OpenID4VP`
                        /// spec
                        #[serde(default)]
                        vp_token: Option<Vec<String>>,
                        expires_at: Option<String>,
                    }

                    let body: IdpStatus = response
                        .json()
                        .await
                        .map_err(|e| HttpError::network(e.to_string()))?;

                    // Transform IDP state to SDK RequestStatus
                    let status = match body.state.as_str() {
                        "verified" => {
                            // Join multiple VP tokens with ~ separator (matches WebSocket)
                            // Each token is a complete SD-JWT that can be parsed independently
                            let presentation = body
                                .vp_token
                                .map(|tokens| tokens.join("~"))
                                .unwrap_or_default();
                            RequestStatus::Fulfilled {
                                request_id,
                                presentation,
                                fulfilled_at: body.expires_at.unwrap_or_default(),
                            }
                        }
                        "denied" | "failed" => RequestStatus::Denied {
                            request_id,
                            reason: "User declined or verification failed".to_string(),
                            denied_at: body.expires_at.unwrap_or_default(),
                        },
                        "expired" => RequestStatus::Expired {
                            request_id,
                            expired_at: body.expires_at.unwrap_or_default(),
                        },
                        // Default to pending for "pending" and unknown states
                        _ => RequestStatus::Pending {
                            request_id,
                            created_at: body.expires_at.unwrap_or_default(),
                        },
                    };
                    Ok(status)
                } else if response.status().as_u16() == 410 {
                    // 410 Gone = expired
                    Ok(RequestStatus::Expired {
                        request_id,
                        expired_at: String::new(),
                    })
                } else if response.status().as_u16() == 404 {
                    // 404 Not Found = request doesn't exist (terminal state, stop polling)
                    let message = response.text().await.unwrap_or_default();
                    Ok(RequestStatus::NotFound {
                        request_id,
                        message,
                    })
                } else {
                    Err(Self::response_to_error(response).await)
                }
            }
        })
        .await
    }

    async fn get_presentation(
        &self,
        request_id: &RequestId,
        client_id: &str,
    ) -> Result<VerifiablePresentation, HttpError> {
        // Get presentation from status endpoint (vp_token field)
        let status = self.poll_status(request_id, client_id).await?;

        match status {
            RequestStatus::Fulfilled { presentation, .. } => {
                // Presentation may contain multiple SD-JWTs joined by ~.
                // Parse the first one. For multiple credentials, callers should
                // access RequestStatus::Fulfilled.presentation directly and use
                // split_vp_tokens() to separate them.
                let first_token = split_vp_tokens(&presentation)
                    .into_iter()
                    .next()
                    .unwrap_or(presentation);
                VerifiablePresentation::parse(&first_token)
                    .map_err(|e| HttpError::network(format!("Failed to parse presentation: {e}")))
            }
            RequestStatus::Pending { .. } => {
                Err(HttpError::status(404, "Request not yet fulfilled"))
            }
            RequestStatus::Denied { reason, .. } => Err(HttpError::status(403, reason)),
            RequestStatus::Expired { .. } => Err(HttpError::status(410, "Request expired")),
            RequestStatus::NotFound { message, .. } => Err(HttpError::status(404, message)),
            RequestStatus::Timeout { .. } => Err(HttpError::status(408, "Request timed out")),
        }
    }
}

impl Clone for HttpIdpClient {
    fn clone(&self) -> Self {
        Self {
            http_client: self.http_client.clone(),
            base_url: self.base_url.clone(),
            retry_policy: self.retry_policy.clone(),
            correlation_config: self.correlation_config.clone(),
        }
    }
}

#[cfg(any(test, feature = "test-utils"))]
pub mod mock {
    //! Mock IDP client for testing.

    use std::collections::VecDeque;
    use std::sync::Mutex;

    use super::{
        CredentialPolicyRequest, CredentialRequestResponse, HttpError, IdpClient, RequestId,
        RequestStatus, VerifiablePresentation, async_trait,
    };

    /// Mock IDP client for testing.
    pub struct MockIdpClient {
        requests: Mutex<VecDeque<Result<CredentialRequestResponse, HttpError>>>,
        polls: Mutex<VecDeque<Result<RequestStatus, HttpError>>>,
        presentations: Mutex<VecDeque<Result<VerifiablePresentation, HttpError>>>,
    }

    impl MockIdpClient {
        /// Create a new mock IDP client.
        #[must_use]
        #[allow(clippy::missing_const_for_fn)] // Mutex::new is not const in stable Rust
        pub fn new() -> Self {
            Self {
                requests: Mutex::new(VecDeque::new()),
                polls: Mutex::new(VecDeque::new()),
                presentations: Mutex::new(VecDeque::new()),
            }
        }

        /// Add an expected response for `request_credential`.
        pub fn expect_request(&self, response: Result<CredentialRequestResponse, HttpError>) {
            self.requests.lock().unwrap().push_back(response);
        }

        /// Add an expected response for `poll_status`.
        pub fn expect_poll(&self, response: Result<RequestStatus, HttpError>) {
            self.polls.lock().unwrap().push_back(response);
        }

        /// Add an expected response for `get_presentation`.
        pub fn expect_presentation(&self, response: Result<VerifiablePresentation, HttpError>) {
            self.presentations.lock().unwrap().push_back(response);
        }
    }

    impl Default for MockIdpClient {
        fn default() -> Self {
            Self::new()
        }
    }

    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
    impl IdpClient for MockIdpClient {
        async fn request_credential(
            &self,
            _policy: CredentialPolicyRequest,
            _bearer_token: &str,
            _dpop_proof: &str,
        ) -> Result<CredentialRequestResponse, HttpError> {
            self.requests
                .lock()
                .unwrap()
                .pop_front()
                .unwrap_or_else(|| Err(HttpError::network("Mock not configured")))
        }

        async fn poll_status(
            &self,
            _request_id: &RequestId,
            _client_id: &str,
        ) -> Result<RequestStatus, HttpError> {
            self.polls
                .lock()
                .unwrap()
                .pop_front()
                .unwrap_or_else(|| Err(HttpError::network("Mock not configured")))
        }

        async fn get_presentation(
            &self,
            _request_id: &RequestId,
            _client_id: &str,
        ) -> Result<VerifiablePresentation, HttpError> {
            self.presentations
                .lock()
                .unwrap()
                .pop_front()
                .unwrap_or_else(|| Err(HttpError::network("Mock not configured")))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::credential_chain::types;

    #[test]
    fn test_build_verification_request_base_uses_const() {
        // Base has single-element chain, so should use const (not enum)
        let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
        policy.client_id = "test-client".to_string();

        let request = build_verification_request(&policy);

        // Should have response_type = "vp_token"
        assert_eq!(request["response_type"], "vp_token");
        assert_eq!(request["response_mode"], "direct_post");
        assert_eq!(request["client_id"], "test-client");

        // Should have SINGLE input_descriptor
        let descriptors = &request["presentation_definition"]["input_descriptors"];
        assert!(descriptors.is_array());
        assert_eq!(descriptors.as_array().unwrap().len(), 1);
        assert_eq!(descriptors[0]["id"], "credential");

        // Single type should use const, not enum
        let filter = &descriptors[0]["constraints"]["fields"][0]["filter"];
        assert_eq!(filter["type"], "string");
        assert_eq!(filter["const"], types::BASE);
        assert!(
            filter["enum"].is_null(),
            "Single type should use const, not enum"
        );
    }

    #[test]
    fn test_build_verification_request_verified_email_uses_enum() {
        // VerifiedEmail chain: [Base, VerifiedEmail]
        let mut policy = CredentialPolicyRequest::with_chain(types::VERIFIED_EMAIL);
        policy.client_id = "test-client".to_string();

        let request = build_verification_request(&policy);

        // Should have SINGLE input_descriptor (not multiple)
        let descriptors = &request["presentation_definition"]["input_descriptors"];
        let descriptors_arr = descriptors.as_array().unwrap();
        assert_eq!(descriptors_arr.len(), 1);
        assert_eq!(descriptors_arr[0]["id"], "credential");

        // Should have enum filter with the full chain
        let filter = &descriptors_arr[0]["constraints"]["fields"][0]["filter"];
        assert_eq!(filter["type"], "string");
        let enum_values = filter["enum"].as_array().expect("Should have enum filter");
        assert_eq!(enum_values.len(), 2);
        assert_eq!(enum_values[0], types::BASE);
        assert_eq!(enum_values[1], types::VERIFIED_EMAIL);
    }

    #[test]
    fn test_build_verification_request_employment_uses_enum() {
        // Employment chain: [Base, Persona, Employment]
        let mut policy = CredentialPolicyRequest::with_chain(types::EMPLOYMENT);
        policy.client_id = "test-client".to_string();

        let request = build_verification_request(&policy);

        // Should have SINGLE input_descriptor
        let descriptors = &request["presentation_definition"]["input_descriptors"];
        let descriptors_arr = descriptors.as_array().unwrap();
        assert_eq!(descriptors_arr.len(), 1);

        // Should have enum filter with the full chain
        let filter = &descriptors_arr[0]["constraints"]["fields"][0]["filter"];
        let enum_values = filter["enum"].as_array().expect("Should have enum filter");
        assert_eq!(enum_values.len(), 3);
        assert_eq!(enum_values[0], types::BASE);
        assert_eq!(enum_values[1], types::PERSONA);
        assert_eq!(enum_values[2], types::EMPLOYMENT);
    }

    #[test]
    fn test_build_verification_request_with_constraints() {
        // VerifiedEmail chain with constraints
        let mut policy = CredentialPolicyRequest::with_chain(types::VERIFIED_EMAIL);
        policy.client_id = "test-client".to_string();
        policy.add_constraint("domain", serde_json::json!({"pattern": ".*@example.com"}));

        let request = build_verification_request(&policy);

        // Single descriptor with vct + domain constraint
        let descriptors = &request["presentation_definition"]["input_descriptors"];
        let fields = descriptors[0]["constraints"]["fields"].as_array().unwrap();
        assert_eq!(fields.len(), 2);

        // First field: vct with enum
        assert_eq!(fields[0]["path"][0], "$.vct");
        assert!(fields[0]["filter"]["enum"].is_array());

        // Second field: domain constraint
        assert_eq!(fields[1]["path"][0], "$.domain");
        assert_eq!(fields[1]["filter"]["pattern"], ".*@example.com");
    }

    #[test]
    fn test_build_verification_request_no_extra_fields() {
        let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
        policy.client_id = "test-client".to_string();

        let request = build_verification_request(&policy);

        let descriptor = &request["presentation_definition"]["input_descriptors"][0];

        // Should NOT have format, name, or purpose fields
        assert!(
            descriptor["format"].is_null(),
            "Should not have 'format' field"
        );
        assert!(descriptor["name"].is_null(), "Should not have 'name' field");
        assert!(
            descriptor["purpose"].is_null(),
            "Should not have 'purpose' field"
        );

        // Should have id and constraints
        assert_eq!(descriptor["id"], "credential");
        assert!(!descriptor["constraints"].is_null());
    }

    #[test]
    fn test_build_verification_request_with_callback() {
        let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
        policy.client_id = "test-client".to_string();
        policy.callback_url = Some("https://example.com/callback".to_string());

        let request = build_verification_request(&policy);

        assert_eq!(request["redirect_uri"], "https://example.com/callback");
    }

    #[test]
    fn test_build_verification_request_static_id() {
        let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
        policy.client_id = "test-client".to_string();

        let request = build_verification_request(&policy);

        // Should use static "sdk-request" ID (not UUID)
        assert_eq!(request["presentation_definition"]["id"], "sdk-request");
    }

    #[test]
    fn test_build_verification_request_with_state() {
        let policy =
            CredentialPolicyRequest::with_chain(types::BASE).with_state("csrf-token-12345");

        let request = build_verification_request(&policy);

        // State should be a top-level field (NOT in presentation_definition)
        assert_eq!(request["state"], "csrf-token-12345");

        // Verify state is NOT in the field constraints
        let fields =
            &request["presentation_definition"]["input_descriptors"][0]["constraints"]["fields"];
        for field in fields.as_array().unwrap() {
            let path = field["path"][0].as_str().unwrap();
            assert_ne!(path, "$.state", "state should NOT be a field constraint");
        }
    }

    #[test]
    fn test_build_verification_request_with_nonce() {
        let policy = CredentialPolicyRequest::with_chain(types::BASE).with_nonce("nonce-67890");

        let request = build_verification_request(&policy);

        // Nonce should be a top-level field (NOT in presentation_definition)
        assert_eq!(request["nonce"], "nonce-67890");

        // Verify nonce is NOT in the field constraints
        let fields =
            &request["presentation_definition"]["input_descriptors"][0]["constraints"]["fields"];
        for field in fields.as_array().unwrap() {
            let path = field["path"][0].as_str().unwrap();
            assert_ne!(path, "$.nonce", "nonce should NOT be a field constraint");
        }
    }

    #[test]
    fn test_build_verification_request_with_all_openid4vp_fields() {
        let policy = CredentialPolicyRequest::with_chain(types::BASE)
            .with_callback("https://example.com/callback")
            .with_state("csrf-token")
            .with_nonce("binding-nonce");

        let request = build_verification_request(&policy);

        // All OpenID4VP fields should be at top level
        assert_eq!(request["redirect_uri"], "https://example.com/callback");
        assert_eq!(request["state"], "csrf-token");
        assert_eq!(request["nonce"], "binding-nonce");
        assert_eq!(request["response_type"], "vp_token");
        assert_eq!(request["response_mode"], "direct_post");
    }

    #[test]
    fn test_build_verification_request_without_optional_fields() {
        let mut policy = CredentialPolicyRequest::with_chain(types::BASE);
        policy.client_id = "test-client".to_string();
        // Don't set callback_url, state, or nonce

        let request = build_verification_request(&policy);

        // Optional fields should not be present
        assert!(request.get("redirect_uri").is_none() || request["redirect_uri"].is_null());
        assert!(request.get("state").is_none() || request["state"].is_null());
        assert!(request.get("nonce").is_none() || request["nonce"].is_null());

        // Required fields should still be present
        assert_eq!(request["client_id"], "test-client");
        assert_eq!(request["response_type"], "vp_token");
    }

    #[test]
    fn test_build_verification_request_with_required_and_optional_fields() {
        let mut policy = CredentialPolicyRequest::with_chain(types::PERSONA);
        policy.client_id = "test-client".to_string();
        policy.required_fields = Some(vec!["display_name".to_string()]);
        policy.optional_fields = Some(vec!["email".to_string(), "phone".to_string()]);

        let request = build_verification_request(&policy);

        let fields =
            &request["presentation_definition"]["input_descriptors"][0]["constraints"]["fields"]
                .as_array()
                .unwrap();

        // Should have vct + 1 required + 2 optional = 4 fields
        assert_eq!(fields.len(), 4);

        // First field: vct filter
        assert_eq!(fields[0]["path"][0], "$.vct");
        assert!(fields[0]["filter"]["enum"].is_array());

        // Second field: required display_name (no optional flag)
        assert_eq!(fields[1]["path"][0], "$.display_name");
        assert!(
            fields[1].get("optional").is_none() || fields[1]["optional"].is_null(),
            "Required field should not have optional flag"
        );

        // Third field: optional email
        assert_eq!(fields[2]["path"][0], "$.email");
        assert_eq!(fields[2]["optional"], true);

        // Fourth field: optional phone
        assert_eq!(fields[3]["path"][0], "$.phone");
        assert_eq!(fields[3]["optional"], true);
    }

    #[test]
    fn test_build_verification_request_skips_deprecated_requested_fields() {
        let mut policy = CredentialPolicyRequest::with_chain(types::PERSONA);
        policy.client_id = "test-client".to_string();
        // Add deprecated requestedFields in constraints (should be skipped)
        policy.add_constraint(
            "requestedFields",
            serde_json::json!(["old_field1", "old_field2"]),
        );
        // Add new required_fields
        policy.required_fields = Some(vec!["display_name".to_string()]);

        let request = build_verification_request(&policy);

        let fields =
            &request["presentation_definition"]["input_descriptors"][0]["constraints"]["fields"]
                .as_array()
                .unwrap();

        // Should have vct + display_name only (requestedFields in constraints is skipped)
        assert_eq!(fields.len(), 2);
        assert_eq!(fields[0]["path"][0], "$.vct");
        assert_eq!(fields[1]["path"][0], "$.display_name");

        // Verify no requestedFields filter was added
        for field in fields.iter() {
            let path = field["path"][0].as_str().unwrap();
            assert_ne!(
                path, "$.requestedFields",
                "requestedFields should not appear as a constraint field"
            );
        }
    }
}
