//! Security Scheme Object

use crate::common::helpers::{
    Context, PushError, ValidateWithContext, validate_optional_uri, validate_required_string,
    validate_required_uri,
};
use crate::v3_2::spec::Spec;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};

/// Defines a security scheme that can be used by the operations.
/// Supported schemes are HTTP authentication, an API key (either as a header,
/// a cookie parameter or as a query parameter),
/// OAuth2’s common flows (implicit, password, client credentials and authorization code)
/// as defined in [RFC6749](https://www.rfc-editor.org/rfc/rfc6749.html),
/// and OpenID Connect Discovery.
///
/// Specification Examples:
///
/// * Basic Authentication Sample:
/// ```yaml
/// type: http
/// scheme: basic
/// ```
///
/// * API Key Sample:
/// ```yaml
/// type: apiKey
/// name: api_key
/// in: header
/// ```
///
/// * JWT Bearer Sample
/// ```yaml
/// type: http
/// scheme: bearer
/// bearerFormat: JWT
/// ```
///
/// * Implicit OAuth2 Sample:
/// ```yaml
/// type: oauth2
/// flows:
///   implicit:
///     authorizationUrl: https://example.com/api/oauth/dialog
///     scopes:
///       write:pets: modify pets in your account
///       read:pets: read your pets
/// ```
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum SecurityScheme {
    /// Basic Authentication Type
    #[serde(rename = "http")]
    HTTP(Box<HttpSecurityScheme>),

    /// API Key Authentication Type
    #[serde(rename = "apiKey")]
    ApiKey(Box<ApiKeySecurityScheme>),

    /// OAuth2 Authentication Type
    #[serde(rename = "oauth2")]
    OAuth2(Box<OAuth2SecurityScheme>),

    /// OpenIdConnect Authentication Type
    #[serde(rename = "openIdConnect")]
    OpenIdConnect(Box<OpenIdConnectSecurityScheme>),

    /// Mutual TLS Authentication — added in OAS 3.1.
    /// Uses client certificates; carries no scheme-specific required fields.
    #[serde(rename = "mutualTLS")]
    MutualTLS(Box<MutualTLSSecurityScheme>),
}

impl Display for SecurityScheme {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            SecurityScheme::HTTP(_) => write!(f, "http"),
            SecurityScheme::ApiKey(_) => write!(f, "apiKey"),
            SecurityScheme::OAuth2(_) => write!(f, "oauth2"),
            SecurityScheme::OpenIdConnect(_) => write!(f, "openIdConnect"),
            SecurityScheme::MutualTLS(_) => write!(f, "mutualTLS"),
        }
    }
}

/// Mutual TLS security scheme (OAS 3.1+).
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct MutualTLSSecurityScheme {
    /// A short description for security scheme. CommonMark allowed.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// `^x-` Specification Extensions.
    /// Whether this security scheme is deprecated and SHOULD be
    /// transitioned out of usage. Default `false`. Added in OAS 3.2.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<bool>,

    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct HttpSecurityScheme {
    /// **Required** The name of the HTTP Authorization scheme to be used in
    /// the Authorization header as defined in
    /// [RFC7235](https://www.rfc-editor.org/rfc/rfc7235). Per OAS 3.1 the
    /// value is a free-form string; values **SHOULD** be registered in the
    /// [IANA Authentication Scheme registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml)
    /// (a SHOULD, not a MUST), so we accept any string here. Comparisons
    /// are case-insensitive per the spec.
    pub scheme: String,

    /// A hint to the client to identify how the bearer token is formatted.
    /// Bearer tokens are usually generated by an authorization server,
    /// so this information is primarily for documentation purposes.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "bearerFormat")]
    pub bearer_format: Option<String>,

    /// A short description for security scheme.
    /// [CommonMark](https://spec.commonmark.org) syntax MAY be used for rich text representation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// This object MAY be extended with Specification Extensions.
    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
    /// The value can be null, a primitive, an array or an object.
    /// Whether this security scheme is deprecated and SHOULD be
    /// transitioned out of usage. Default `false`. Added in OAS 3.2.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<bool>,

    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

/// String constants for the registered HTTP Authorization schemes from
/// the [IANA Authentication Scheme registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml).
/// Per OAS 3.1 the `scheme` field is a free-form string; these constants
/// are convenience values for the registered set. Comparison should be
/// case-insensitive (RFC 7235 Section 2.1).
pub mod http_scheme {
    pub const BASIC: &str = "Basic";
    pub const BEARER: &str = "Bearer";
    pub const DIGEST: &str = "Digest";
    pub const DPOP: &str = "DPoP";
    pub const HOBA: &str = "HOBA";
    pub const MUTUAL: &str = "Mutual";
    pub const NEGOTIATE: &str = "Negotiate";
    pub const OAUTH: &str = "OAuth";
    pub const SCRAM_SHA_1: &str = "SCRAM-SHA-1";
    pub const SCRAM_SHA_256: &str = "SCRAM-SHA-256";
    pub const VAPID: &str = "vapid";
}

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct ApiKeySecurityScheme {
    /// **Required** A short description for security scheme.
    pub name: String,

    /// **Required** The location of the API key.
    #[serde(rename = "in")]
    pub location: ApiKeyLocation,

    /// A short description for security scheme.
    /// [CommonMark](https://spec.commonmark.org) syntax MAY be used for rich text representation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// This object MAY be extended with Specification Extensions.
    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
    /// The value can be null, a primitive, an array or an object.
    /// Whether this security scheme is deprecated and SHOULD be
    /// transitioned out of usage. Default `false`. Added in OAS 3.2.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<bool>,

    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

/// The location of the API key.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub enum ApiKeyLocation {
    /// The API key MUST be transmitted as a header parameter.
    #[default]
    #[serde(rename = "query")]
    Query,

    /// The API key MUST be transmitted as a query parameter.
    #[serde(rename = "header")]
    Header,

    /// The API key MUST be transmitted as a cookie parameter.
    #[serde(rename = "cookie")]
    Cookie,
}

impl Display for ApiKeyLocation {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            ApiKeyLocation::Query => write!(f, "query"),
            ApiKeyLocation::Header => write!(f, "header"),
            ApiKeyLocation::Cookie => write!(f, "cookie"),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct OAuth2SecurityScheme {
    /// **Required**  An object containing configuration information for the flow types supported.
    pub flows: OAuth2Flows,

    /// A short description for security scheme.
    /// [CommonMark](https://spec.commonmark.org) syntax MAY be used for rich text representation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// URL of the OAuth 2.0 Authorization Server Metadata document
    /// (RFC 8414), added in OAS 3.2.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "oauth2MetadataUrl")]
    pub oauth2_metadata_url: Option<String>,

    /// This object MAY be extended with Specification Extensions.
    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
    /// The value can be null, a primitive, an array or an object.
    /// Whether this security scheme is deprecated and SHOULD be
    /// transitioned out of usage. Default `false`. Added in OAS 3.2.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<bool>,

    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

/// Allows configuration of the supported OAuth Flows.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct OAuth2Flows {
    /// Configuration for the OAuth Implicit flow
    #[serde(skip_serializing_if = "Option::is_none")]
    pub implicit: Option<ImplicitOAuth2Flow>,

    /// Configuration for the OAuth Resource Owner Password flow
    #[serde(skip_serializing_if = "Option::is_none")]
    pub password: Option<PasswordOAuth2Flow>,

    /// Configuration for the OAuth Client Credentials flow.
    /// Previously called `application` in OpenAPI 2.0.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "clientCredentials")]
    pub client_credentials: Option<ClientCredentialsOAuth2Flow>,

    /// Configuration for the OAuth Authorization Code flow.
    /// Previously called `accessCode` in OpenAPI 2.0.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "authorizationCode")]
    pub authorization_code: Option<AuthorizationCodeOAuth2Flow>,

    /// Configuration for the OAuth 2.0 Device Authorization Grant
    /// (RFC 8628). Added in OAS 3.2.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "deviceAuthorization")]
    pub device_authorization: Option<DeviceAuthorizationOAuth2Flow>,

    /// This object MAY be extended with Specification Extensions.
    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
    /// The value can be null, a primitive, an array or an object.
    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

/// Configuration details for the Implicit OAuth Flow
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct ImplicitOAuth2Flow {
    /// **Required** The authorization URL to be used for this flow.
    /// This MUST be in the form of a URL.
    #[serde(rename = "authorizationUrl")]
    pub authorization_url: String,

    /// The URL to be used for obtaining refresh tokens.
    /// This MUST be in the form of a URL.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "refreshUrl")]
    pub refresh_url: Option<String>,

    /// **Required** The available scopes for the OAuth2 security scheme.
    /// A map between the scope name and a short description for it.
    /// The map MAY be empty.
    pub scopes: BTreeMap<String, String>,

    /// This object MAY be extended with Specification Extensions.
    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
    /// The value can be null, a primitive, an array or an object.
    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

/// Configuration details for the Password OAuth Flow
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct PasswordOAuth2Flow {
    /// **Required** The token URL to be used for this flow.
    /// This MUST be in the form of a URL.
    #[serde(rename = "tokenUrl")]
    pub token_url: String,

    /// The URL to be used for obtaining refresh tokens.
    /// This MUST be in the form of a URL.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "refreshUrl")]
    pub refresh_url: Option<String>,

    /// **Required** The available scopes for the OAuth2 security scheme.
    /// A map between the scope name and a short description for it.
    /// The map MAY be empty.
    pub scopes: BTreeMap<String, String>,

    /// This object MAY be extended with Specification Extensions.
    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
    /// The value can be null, a primitive, an array or an object.
    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

/// Configuration details for the ClientCredentials OAuth Flow
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct ClientCredentialsOAuth2Flow {
    /// **Required** The token URL to be used for this flow.
    /// This MUST be in the form of a URL.
    #[serde(rename = "tokenUrl")]
    pub token_url: String,

    /// The URL to be used for obtaining refresh tokens.
    /// This MUST be in the form of a URL.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "refreshUrl")]
    pub refresh_url: Option<String>,

    /// **Required** The available scopes for the OAuth2 security scheme.
    /// A map between the scope name and a short description for it.
    /// The map MAY be empty.
    pub scopes: BTreeMap<String, String>,

    /// This object MAY be extended with Specification Extensions.
    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
    /// The value can be null, a primitive, an array or an object.
    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

/// Configuration details for the OAuth 2.0 Device Authorization Grant
/// (RFC 8628). Added in OAS 3.2.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct DeviceAuthorizationOAuth2Flow {
    /// **Required** The device-authorization endpoint URL.
    #[serde(rename = "deviceAuthorizationUrl")]
    pub device_authorization_url: String,

    /// **Required** The token URL.
    #[serde(rename = "tokenUrl")]
    pub token_url: String,

    /// The URL to be used for obtaining refresh tokens. MUST be a URL.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "refreshUrl")]
    pub refresh_url: Option<String>,

    /// **Required** The available scopes for the OAuth2 security scheme.
    pub scopes: BTreeMap<String, String>,

    /// `^x-` Specification Extensions.
    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

/// Configuration details for the ClientCredentials OAuth Flow
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct AuthorizationCodeOAuth2Flow {
    /// **Required** The authorization URL to be used for this flow.
    /// This MUST be in the form of a URL.
    #[serde(rename = "authorizationUrl")]
    pub authorization_url: String,

    /// **Required** The token URL to be used for this flow.
    /// This MUST be in the form of a URL.
    #[serde(rename = "tokenUrl")]
    pub token_url: String,

    /// The URL to be used for obtaining refresh tokens.
    /// This MUST be in the form of a URL.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "refreshUrl")]
    pub refresh_url: Option<String>,

    /// **Required** The available scopes for the OAuth2 security scheme.
    /// A map between the scope name and a short description for it.
    /// The map MAY be empty.
    pub scopes: BTreeMap<String, String>,

    /// This object MAY be extended with Specification Extensions.
    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
    /// The value can be null, a primitive, an array or an object.
    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct OpenIdConnectSecurityScheme {
    /// **Required** OpenId Connect URL to discover OAuth2 configuration values.
    /// This MUST be in the form of a URL.
    #[serde(rename = "openIdConnectUrl")]
    pub open_id_connect_url: String,

    /// A short description for security scheme.
    /// [CommonMark](https://spec.commonmark.org) syntax MAY be used for rich text representation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// This object MAY be extended with Specification Extensions.
    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
    /// The value can be null, a primitive, an array or an object.
    /// Whether this security scheme is deprecated and SHOULD be
    /// transitioned out of usage. Default `false`. Added in OAS 3.2.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<bool>,

    #[serde(flatten)]
    #[serde(with = "crate::common::extensions")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}

impl ValidateWithContext<Spec> for SecurityScheme {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        match self {
            SecurityScheme::HTTP(http) => http.validate_with_context(ctx, path),
            SecurityScheme::ApiKey(api_key) => api_key.validate_with_context(ctx, path),
            SecurityScheme::OAuth2(oauth2) => oauth2.validate_with_context(ctx, path),
            SecurityScheme::OpenIdConnect(open_id_connect) => {
                open_id_connect.validate_with_context(ctx, path)
            }
            SecurityScheme::MutualTLS(mtls) => mtls.validate_with_context(ctx, path),
        }
    }
}

impl ValidateWithContext<Spec> for MutualTLSSecurityScheme {
    fn validate_with_context(&self, _ctx: &mut Context<Spec>, _path: String) {
        // No scheme-specific fields beyond description / extensions; nothing
        // to validate here.
    }
}

impl ValidateWithContext<Spec> for HttpSecurityScheme {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        // Spec: `scheme` is REQUIRED for `type: http`. The struct holds it
        // as `String` (default = ""), so check non-empty here.
        validate_required_string(&self.scheme, ctx, format!("{path}.scheme"));

        if let Some(bearer_format) = &self.bearer_format
            && !bearer_format.is_empty()
            && !self.scheme.eq_ignore_ascii_case("bearer")
        {
            ctx.error(
                path,
                format_args!(".bearerFormat: must be empty for scheme `{}`", self.scheme),
            );
        }
    }
}

impl ValidateWithContext<Spec> for ApiKeySecurityScheme {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        validate_required_string(&self.name, ctx, format!("{path}.name"));
    }
}

impl ValidateWithContext<Spec> for OAuth2SecurityScheme {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        self.flows
            .validate_with_context(ctx, format!("{path}.flow"));
        validate_optional_uri(
            &self.oauth2_metadata_url,
            ctx,
            format!("{path}.oauth2MetadataUrl"),
        );
    }
}

impl ValidateWithContext<Spec> for OAuth2Flows {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        // Per the OAS 3.2 JSON Schema, `flows` does not require any of
        // implicit / password / clientCredentials / authorizationCode /
        // deviceAuthorization to be set — an empty `flows: {}` is valid.
        if let Some(flow) = &self.implicit {
            flow.validate_with_context(ctx, format!("{path}.implicit"));
        }
        if let Some(flow) = &self.password {
            flow.validate_with_context(ctx, format!("{path}.password"));
        }
        if let Some(flow) = &self.client_credentials {
            flow.validate_with_context(ctx, format!("{path}.clientCredentials"));
        }
        if let Some(flow) = &self.authorization_code {
            flow.validate_with_context(ctx, format!("{path}.authorizationCode"));
        }
        if let Some(flow) = &self.device_authorization {
            flow.validate_with_context(ctx, format!("{path}.deviceAuthorization"));
        }
    }
}

impl ValidateWithContext<Spec> for ImplicitOAuth2Flow {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        validate_required_uri(
            &self.authorization_url,
            ctx,
            format!("{path}.authorizationUrl"),
        );
        validate_optional_uri(&self.refresh_url, ctx, format!("{path}.refreshUrl"));
    }
}

impl ValidateWithContext<Spec> for PasswordOAuth2Flow {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        validate_required_uri(&self.token_url, ctx, format!("{path}.tokenUrl"));
        validate_optional_uri(&self.refresh_url, ctx, format!("{path}.refreshUrl"));
    }
}

impl ValidateWithContext<Spec> for ClientCredentialsOAuth2Flow {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        validate_required_uri(&self.token_url, ctx, format!("{path}.tokenUrl"));
        validate_optional_uri(&self.refresh_url, ctx, format!("{path}.refreshUrl"));
    }
}

impl ValidateWithContext<Spec> for AuthorizationCodeOAuth2Flow {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        validate_required_uri(
            &self.authorization_url,
            ctx,
            format!("{path}.authorizationUrl"),
        );
        validate_required_uri(&self.token_url, ctx, format!("{path}.tokenUrl"));
        validate_optional_uri(&self.refresh_url, ctx, format!("{path}.refreshUrl"));
    }
}

impl ValidateWithContext<Spec> for DeviceAuthorizationOAuth2Flow {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        validate_required_uri(
            &self.device_authorization_url,
            ctx,
            format!("{path}.deviceAuthorizationUrl"),
        );
        validate_required_uri(&self.token_url, ctx, format!("{path}.tokenUrl"));
        validate_optional_uri(&self.refresh_url, ctx, format!("{path}.refreshUrl"));
    }
}

impl ValidateWithContext<Spec> for OpenIdConnectSecurityScheme {
    fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
        validate_required_uri(
            &self.open_id_connect_url,
            ctx,
            format!("{path}.openIdConnectUrl"),
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::validation::Options;
    use serde_json::json;

    #[test]
    fn test_security_scheme_http_deserialize() {
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "Basic",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Basic".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = basic",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "Bearer",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Bearer".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = bearer",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "Digest",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Digest".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = digest",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "DPoP",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "DPoP".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = dpop",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "HOBA",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "HOBA".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = hoba",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "Mutual",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Mutual".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = mutual",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "Negotiate",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Negotiate".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = negotiate",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "OAuth",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "OAuth".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = oauth",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "SCRAM-SHA-1",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "SCRAM-SHA-1".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = scram-sha-1",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "SCRAM-SHA-256",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "SCRAM-SHA-256".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = scram-sha-2256",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "http",
                "scheme": "vapid",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "vapid".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize scheme = vapid",
        );
    }

    #[test]
    fn test_security_scheme_http_serialize() {
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Basic".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "Basic",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = basic",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Bearer".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "Bearer",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = bearer",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Digest".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "Digest",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = digest",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "DPoP".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "DPoP",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = dpop",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "HOBA".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "HOBA",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = hoba",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Mutual".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "Mutual",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = mutual",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "Negotiate".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "Negotiate",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = negotiate",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "OAuth".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "OAuth",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = oauth",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "SCRAM-SHA-1".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "SCRAM-SHA-1",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = scram-sha-1",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "SCRAM-SHA-256".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "SCRAM-SHA-256",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = scram-sha-256",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
                scheme: "vapid".to_owned(),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "http",
                "scheme": "vapid",
                "description": "A short description for security scheme.",
            }),
            "serialize scheme = vapid",
        );
    }

    #[test]
    fn test_security_scheme_api_key_deserialize() {
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "apiKey",
                "name": "api_key",
                "in": "header",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::ApiKey(Box::new(ApiKeySecurityScheme {
                name: String::from("api_key"),
                location: ApiKeyLocation::Header,
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize in = header",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "apiKey",
                "name": "api_key",
                "in": "query",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::ApiKey(Box::new(ApiKeySecurityScheme {
                name: String::from("api_key"),
                location: ApiKeyLocation::Query,
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize in = query",
        );
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "apiKey",
                "name": "api_key",
                "in": "cookie",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::ApiKey(Box::new(ApiKeySecurityScheme {
                name: String::from("api_key"),
                location: ApiKeyLocation::Cookie,
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize in = cookie",
        );
    }

    #[test]
    fn test_security_scheme_api_key_serialize() {
        assert_eq!(
            serde_json::to_value(SecurityScheme::ApiKey(Box::new(ApiKeySecurityScheme {
                name: String::from("api_key"),
                location: ApiKeyLocation::Header,
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "apiKey",
                "name": "api_key",
                "in": "header",
                "description": "A short description for security scheme.",
            }),
            "serialize location = header",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::ApiKey(Box::new(ApiKeySecurityScheme {
                name: String::from("api_key"),
                location: ApiKeyLocation::Query,
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "apiKey",
                "name": "api_key",
                "in": "query",
                "description": "A short description for security scheme.",
            }),
            "serialize location = query",
        );
        assert_eq!(
            serde_json::to_value(SecurityScheme::ApiKey(Box::new(ApiKeySecurityScheme {
                name: String::from("api_key"),
                location: ApiKeyLocation::Cookie,
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "apiKey",
                "name": "api_key",
                "in": "cookie",
                "description": "A short description for security scheme.",
            }),
            "serialize location = query",
        );
    }

    #[test]
    fn test_security_scheme_oauth2_deserialize() {
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "oauth2",
                "flows": {
                    "implicit": {
                        "authorizationUrl": "https://example.com/api/oauth/dialog",
                        "scopes": {
                            "write:pets": "modify pets in your account",
                            "read:pets": "read your pets",
                        },
                    },
                    "password": {
                        "tokenUrl": "https://example.com/api/oauth/dialog",
                        "scopes": {
                            "write:pets": "modify pets in your account",
                            "read:pets": "read your pets",
                        },
                    },
                    "clientCredentials": {
                        "tokenUrl": "https://example.com/api/oauth/dialog",
                        "scopes": {
                            "write:pets": "modify pets in your account",
                            "read:pets": "read your pets",
                        },
                    },
                    "authorizationCode": {
                        "authorizationUrl": "https://example.com/api/oauth/dialog",
                        "tokenUrl": "https://example.com/api/oauth/dialog",
                        "scopes": {
                            "write:pets": "modify pets in your account",
                            "read:pets": "read your pets",
                        },
                    },
                },
                "description": "A short description for security scheme.",
                "x-tra": "custom",
            }))
            .unwrap(),
            SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
                flows: OAuth2Flows {
                    implicit: Some(ImplicitOAuth2Flow {
                        authorization_url: String::from("https://example.com/api/oauth/dialog"),
                        scopes: BTreeMap::from_iter(vec![
                            (
                                String::from("write:pets"),
                                String::from("modify pets in your account"),
                            ),
                            (String::from("read:pets"), String::from("read your pets"),),
                        ]),
                        ..Default::default()
                    }),
                    password: Some(PasswordOAuth2Flow {
                        token_url: String::from("https://example.com/api/oauth/dialog"),
                        scopes: BTreeMap::from_iter(vec![
                            (
                                String::from("write:pets"),
                                String::from("modify pets in your account"),
                            ),
                            (String::from("read:pets"), String::from("read your pets"),),
                        ]),
                        ..Default::default()
                    }),
                    client_credentials: Some(ClientCredentialsOAuth2Flow {
                        token_url: String::from("https://example.com/api/oauth/dialog"),
                        scopes: BTreeMap::from_iter(vec![
                            (
                                String::from("write:pets"),
                                String::from("modify pets in your account"),
                            ),
                            (String::from("read:pets"), String::from("read your pets"),),
                        ]),
                        ..Default::default()
                    }),
                    authorization_code: Some(AuthorizationCodeOAuth2Flow {
                        authorization_url: String::from("https://example.com/api/oauth/dialog"),
                        token_url: String::from("https://example.com/api/oauth/dialog"),
                        scopes: BTreeMap::from_iter(vec![
                            (
                                String::from("write:pets"),
                                String::from("modify pets in your account"),
                            ),
                            (String::from("read:pets"), String::from("read your pets"),),
                        ]),
                        ..Default::default()
                    }),
                    ..Default::default()
                },
                description: Some(String::from("A short description for security scheme.")),
                deprecated: None,
                oauth2_metadata_url: None,
                extensions: Some({
                    let mut map = BTreeMap::new();
                    map.insert(
                        String::from("x-tra"),
                        serde_json::Value::String(String::from("custom")),
                    );
                    map
                }),
            })),
            "deserialize",
        );
    }

    #[test]
    fn test_security_scheme_oauth2_serialize() {
        assert_eq!(
            serde_json::to_value(SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
                flows: OAuth2Flows {
                    implicit: Some(ImplicitOAuth2Flow {
                        authorization_url: String::from("https://example.com/api/oauth/dialog"),
                        scopes: BTreeMap::from_iter(vec![
                            (
                                String::from("write:pets"),
                                String::from("modify pets in your account"),
                            ),
                            (String::from("read:pets"), String::from("read your pets"),),
                        ]),
                        ..Default::default()
                    }),
                    password: Some(PasswordOAuth2Flow {
                        token_url: String::from("https://example.com/api/oauth/dialog"),
                        scopes: BTreeMap::from_iter(vec![
                            (
                                String::from("write:pets"),
                                String::from("modify pets in your account"),
                            ),
                            (String::from("read:pets"), String::from("read your pets"),),
                        ]),
                        ..Default::default()
                    }),
                    client_credentials: Some(ClientCredentialsOAuth2Flow {
                        token_url: String::from("https://example.com/api/oauth/dialog"),
                        scopes: BTreeMap::from_iter(vec![
                            (
                                String::from("write:pets"),
                                String::from("modify pets in your account"),
                            ),
                            (String::from("read:pets"), String::from("read your pets"),),
                        ]),
                        ..Default::default()
                    }),
                    authorization_code: Some(AuthorizationCodeOAuth2Flow {
                        authorization_url: String::from("https://example.com/api/oauth/dialog"),
                        token_url: String::from("https://example.com/api/oauth/dialog"),
                        scopes: BTreeMap::from_iter(vec![
                            (
                                String::from("write:pets"),
                                String::from("modify pets in your account"),
                            ),
                            (String::from("read:pets"), String::from("read your pets"),),
                        ]),
                        ..Default::default()
                    }),
                    ..Default::default()
                },
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })))
            .unwrap(),
            json!({
                "type": "oauth2",
                "flows": {
                    "implicit": {
                        "authorizationUrl": "https://example.com/api/oauth/dialog",
                        "scopes": {
                            "write:pets": "modify pets in your account",
                            "read:pets": "read your pets",
                        },
                    },
                    "password": {
                        "tokenUrl": "https://example.com/api/oauth/dialog",
                        "scopes": {
                            "write:pets": "modify pets in your account",
                            "read:pets": "read your pets",
                        },
                    },
                    "clientCredentials": {
                        "tokenUrl": "https://example.com/api/oauth/dialog",
                        "scopes": {
                            "write:pets": "modify pets in your account",
                            "read:pets": "read your pets",
                        },
                    },
                    "authorizationCode": {
                        "authorizationUrl": "https://example.com/api/oauth/dialog",
                        "tokenUrl": "https://example.com/api/oauth/dialog",
                        "scopes": {
                            "write:pets": "modify pets in your account",
                            "read:pets": "read your pets",
                        },
                    },
                },
                "description": "A short description for security scheme.",
            }),
            "serialize",
        );
    }

    #[test]
    fn test_security_scheme_open_id_connect_deserialize() {
        assert_eq!(
            serde_json::from_value::<SecurityScheme>(json!({
                "type": "openIdConnect",
                "openIdConnectUrl": "https://example.com/.well-known/openid-configuration",
                "description": "A short description for security scheme.",
            }))
            .unwrap(),
            SecurityScheme::OpenIdConnect(Box::new(OpenIdConnectSecurityScheme {
                open_id_connect_url: String::from(
                    "https://example.com/.well-known/openid-configuration"
                ),
                description: Some(String::from("A short description for security scheme.")),
                ..Default::default()
            })),
            "deserialize",
        );
    }

    #[test]
    fn test_security_scheme_open_id_connect_serialize() {
        assert_eq!(
            serde_json::to_value(SecurityScheme::OpenIdConnect(Box::new(
                OpenIdConnectSecurityScheme {
                    open_id_connect_url: String::from(
                        "https://example.com/.well-known/openid-configuration"
                    ),
                    description: Some(String::from("A short description for security scheme.")),
                    ..Default::default()
                }
            )))
            .unwrap(),
            json!({
                "type": "openIdConnect",
                "openIdConnectUrl": "https://example.com/.well-known/openid-configuration",
                "description": "A short description for security scheme.",
            }),
            "serialize",
        );
    }

    #[test]
    fn test_security_scheme_validation() {
        let spec = Spec::default();
        let mut ctx = Context::new(&spec, Options::new());

        SecurityScheme::OpenIdConnect(Box::new(OpenIdConnectSecurityScheme {
            open_id_connect_url: String::from(
                "https://example.com/.well-known/openid-configuration",
            ),
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert!(
            ctx.errors.is_empty(),
            "OpenIdConnect: no errors: {:?}",
            ctx.errors
        );

        SecurityScheme::OpenIdConnect(Box::new(OpenIdConnectSecurityScheme {
            open_id_connect_url: String::from(""),
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert_eq!(
            ctx.errors.len(),
            1,
            "OpenIdConnect: one error: {:?}",
            ctx.errors
        );

        ctx = Context::new(&spec, Options::new());
        SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
            scheme: "Basic".to_owned(),
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert!(
            ctx.errors.is_empty(),
            "HTTP::Basic: no errors: {:?}",
            ctx.errors
        );

        SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
            scheme: "Bearer".to_owned(),
            bearer_format: Some(String::from("fooo")),
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert!(
            ctx.errors.is_empty(),
            "HTTP::Bearer with format: no errors: {:?}",
            ctx.errors
        );

        SecurityScheme::HTTP(Box::new(HttpSecurityScheme {
            scheme: "Basic".to_owned(),
            bearer_format: Some(String::from("fooo")),
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert_eq!(
            ctx.errors.len(),
            1,
            "HTTP::Basic with format: one error: {:?}",
            ctx.errors
        );

        ctx = Context::new(&spec, Options::new());
        SecurityScheme::ApiKey(Box::new(ApiKeySecurityScheme {
            name: String::from("api_key"),
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert!(ctx.errors.is_empty(), "ApiKey: no errors: {:?}", ctx.errors);

        SecurityScheme::ApiKey(Box::new(ApiKeySecurityScheme {
            name: String::from(""),
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert_eq!(ctx.errors.len(), 1, "ApiKey: one error: {:?}", ctx.errors);

        ctx = Context::new(&spec, Options::new());
        SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
            flows: OAuth2Flows {
                ..Default::default()
            },
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        // Per the OAS 3.2 JSON Schema, an empty `flows: {}` is valid;
        // no error expected.
        assert!(
            ctx.errors.is_empty(),
            "empty flows accepted: {:?}",
            ctx.errors
        );

        ctx = Context::new(&spec, Options::new());
        SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
            flows: OAuth2Flows {
                implicit: Some(ImplicitOAuth2Flow {
                    authorization_url: String::from("not a uri"),
                    refresh_url: Some(String::from("also not")),
                    ..Default::default()
                }),
                ..Default::default()
            },
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert_eq!(
            ctx.errors.len(),
            2,
            "OAuth2.implicit.authorization_url: two errors: {:?}",
            ctx.errors
        );

        ctx = Context::new(&spec, Options::new());
        SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
            flows: OAuth2Flows {
                password: Some(PasswordOAuth2Flow {
                    token_url: String::from("not a uri"),
                    refresh_url: Some(String::from("also not")),
                    ..Default::default()
                }),
                ..Default::default()
            },
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert_eq!(
            ctx.errors.len(),
            2,
            "OAuth2.password.(token_url, refresh_url): two errors: {:?}",
            ctx.errors
        );

        ctx = Context::new(&spec, Options::new());
        SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
            flows: OAuth2Flows {
                client_credentials: Some(ClientCredentialsOAuth2Flow {
                    token_url: String::from("not a uri"),
                    refresh_url: Some(String::from("also not")),
                    ..Default::default()
                }),
                ..Default::default()
            },
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert_eq!(
            ctx.errors.len(),
            2,
            "OAuth2.client_credentials.(token_url, refresh_url): two errors: {:?}",
            ctx.errors
        );

        ctx = Context::new(&spec, Options::new());
        SecurityScheme::OAuth2(Box::new(OAuth2SecurityScheme {
            flows: OAuth2Flows {
                authorization_code: Some(AuthorizationCodeOAuth2Flow {
                    authorization_url: String::from("third bad"),
                    token_url: String::from("not a uri"),
                    refresh_url: Some(String::from("also not")),
                    ..Default::default()
                }),
                ..Default::default()
            },
            description: Some(String::from("A short description for security scheme.")),
            ..Default::default()
        }))
        .validate_with_context(&mut ctx, String::from("securityScheme"));
        assert_eq!(
            ctx.errors.len(),
            3,
            "OAuth2.authorization_code.(authorization_url, token_url, refresh_url): three errors: {:?}",
            ctx.errors
        );
    }

    #[test]
    fn mutual_tls_round_trip_and_validate() {
        // OAS 3.1 added the mutualTLS scheme. It carries no scheme-specific
        // required fields (only optional `description` + `^x-` extensions).
        let v = serde_json::json!({
            "type": "mutualTLS",
            "description": "client cert auth",
            "x-tra": "yes"
        });
        let s: SecurityScheme = serde_json::from_value(v.clone()).unwrap();
        assert!(matches!(s, SecurityScheme::MutualTLS(_)));
        assert_eq!(serde_json::to_value(&s).unwrap(), v);

        let spec = Spec::default();
        let mut ctx = Context::new(&spec, Options::new());
        s.validate_with_context(&mut ctx, "mtls".into());
        assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
    }
}
