/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use crate::common::MAX_SAFE_INTEGER;
use crate::error::{ErrorStatus, WebDriverError, WebDriverResult};
use serde_json::{Map, Value};
use url::Url;

pub type Capabilities = Map<String, Value>;

/// Trait for objects that can be used to inspect browser capabilities
///
/// The main methods in this trait are called with a Capabilites object
/// resulting from a full set of potential capabilites for the session.  Given
/// those Capabilities they return a property of the browser instance that
/// would be initiated. In many cases this will be independent of the input,
/// but in the case of e.g. browser version, it might depend on a path to the
/// binary provided as a capability.
pub trait BrowserCapabilities {
    /// Set up the Capabilites object
    ///
    /// Typically used to create any internal caches
    fn init(&mut self, _: &Capabilities);

    /// Name of the browser
    fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;

    /// Version number of the browser
    fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;

    /// Compare actual browser version to that provided in a version specifier
    ///
    /// Parameters are the actual browser version and the comparison string,
    /// respectively. The format of the comparison string is
    /// implementation-defined.
    fn compare_browser_version(&mut self, version: &str, comparison: &str)
        -> WebDriverResult<bool>;

    /// Name of the platform/OS
    fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;

    /// Whether insecure certificates are supported
    fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool>;

    /// Indicates whether driver supports all of the window resizing and
    /// repositioning commands.
    fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool>;

    /// Indicates that interactability checks will be applied to `<input type=file>`.
    fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool>;

    /// Whether a WebSocket URL for the created session has to be returned
    fn web_socket_url(&mut self, _: &Capabilities) -> WebDriverResult<bool>;

    /// Indicates whether the endpoint node supports all Virtual Authenticators commands.
    fn webauthn_virtual_authenticators(&mut self, _: &Capabilities) -> WebDriverResult<bool>;

    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the User
    /// Verification Method extension.
    fn webauthn_extension_uvm(&mut self, _: &Capabilities) -> WebDriverResult<bool>;

    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the prf
    /// extension.
    fn webauthn_extension_prf(&mut self, _: &Capabilities) -> WebDriverResult<bool>;

    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the
    /// largeBlob extension.
    fn webauthn_extension_large_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool>;

    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the credBlob
    /// extension.
    fn webauthn_extension_cred_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool>;

    fn accept_proxy(
        &mut self,
        proxy_settings: &Map<String, Value>,
        _: &Capabilities,
    ) -> WebDriverResult<bool>;

    /// Type check custom properties
    ///
    /// Check that custom properties containing ":" have the correct data types.
    /// Properties that are unrecognised must be ignored i.e. return without
    /// error.
    fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()>;

    /// Check if custom properties are accepted capabilites
    ///
    /// Check that custom properties containing ":" are compatible with
    /// the implementation.
    fn accept_custom(
        &mut self,
        name: &str,
        value: &Value,
        merged: &Capabilities,
    ) -> WebDriverResult<bool>;
}

/// Trait to abstract over various version of the new session parameters
///
/// This trait is expected to be implemented on objects holding the capabilities
/// from a new session command.
pub trait CapabilitiesMatching {
    /// Match the BrowserCapabilities against some candidate capabilites
    ///
    /// Takes a BrowserCapabilites object and returns a set of capabilites that
    /// are valid for that browser, if any, or None if there are no matching
    /// capabilities.
    fn match_browser<T: BrowserCapabilities>(
        &self,
        browser_capabilities: &mut T,
    ) -> WebDriverResult<Option<Capabilities>>;
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct SpecNewSessionParameters {
    #[serde(default = "Capabilities::default")]
    pub alwaysMatch: Capabilities,
    #[serde(default = "firstMatch_default")]
    pub firstMatch: Vec<Capabilities>,
}

impl Default for SpecNewSessionParameters {
    fn default() -> Self {
        SpecNewSessionParameters {
            alwaysMatch: Capabilities::new(),
            firstMatch: vec![Capabilities::new()],
        }
    }
}

fn firstMatch_default() -> Vec<Capabilities> {
    vec![Capabilities::default()]
}

impl SpecNewSessionParameters {
    fn validate<T: BrowserCapabilities>(
        &self,
        mut capabilities: Capabilities,
        browser_capabilities: &mut T,
    ) -> WebDriverResult<Capabilities> {
        // Filter out entries with the value `null`
        let null_entries = capabilities
            .iter()
            .filter(|&(_, value)| *value == Value::Null)
            .map(|(k, _)| k.clone())
            .collect::<Vec<String>>();
        for key in null_entries {
            capabilities.remove(&key);
        }

        for (key, value) in &capabilities {
            match &**key {
                x @ "acceptInsecureCerts"
                | x @ "setWindowRect"
                | x @ "strictFileInteractability"
                | x @ "webSocketUrl"
                | x @ "webauthn:virtualAuthenticators"
                | x @ "webauthn:extension:uvm"
                | x @ "webauthn:extension:prf"
                | x @ "webauthn:extension:largeBlob"
                | x @ "webauthn:extension:credBlob" => {
                    if !value.is_boolean() {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidArgument,
                            format!("{} is not boolean: {}", x, value),
                        ));
                    }
                }
                x @ "browserName" | x @ "browserVersion" | x @ "platformName" => {
                    if !value.is_string() {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidArgument,
                            format!("{} is not a string: {}", x, value),
                        ));
                    }
                }
                "pageLoadStrategy" => SpecNewSessionParameters::validate_page_load_strategy(value)?,
                "proxy" => SpecNewSessionParameters::validate_proxy(value)?,
                "timeouts" => SpecNewSessionParameters::validate_timeouts(value)?,
                "unhandledPromptBehavior" => {
                    SpecNewSessionParameters::validate_unhandled_prompt_behavior(value)?
                }
                x => {
                    if !x.contains(':') {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidArgument,
                            format!(
                                "{} is not the name of a known capability or extension capability",
                                x
                            ),
                        ));
                    } else {
                        browser_capabilities.validate_custom(x, value)?
                    }
                }
            }
        }

        // With a value of `false` the capability needs to be removed.
        if let Some(Value::Bool(false)) = capabilities.get(&"webSocketUrl".to_string()) {
            capabilities.remove(&"webSocketUrl".to_string());
        }

        Ok(capabilities)
    }

    fn validate_page_load_strategy(value: &Value) -> WebDriverResult<()> {
        match value {
            Value::String(x) => match &**x {
                "normal" | "eager" | "none" => {}
                x => {
                    return Err(WebDriverError::new(
                        ErrorStatus::InvalidArgument,
                        format!("Invalid page load strategy: {}", x),
                    ))
                }
            },
            _ => {
                return Err(WebDriverError::new(
                    ErrorStatus::InvalidArgument,
                    "pageLoadStrategy is not a string",
                ))
            }
        }
        Ok(())
    }

    fn validate_proxy(proxy_value: &Value) -> WebDriverResult<()> {
        let obj = try_opt!(
            proxy_value.as_object(),
            ErrorStatus::InvalidArgument,
            "proxy is not an object"
        );

        for (key, value) in obj {
            match &**key {
                "proxyType" => match value.as_str() {
                    Some("pac") | Some("direct") | Some("autodetect") | Some("system")
                    | Some("manual") => {}
                    Some(x) => {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidArgument,
                            format!("Invalid proxyType value: {}", x),
                        ))
                    }
                    None => {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidArgument,
                            format!("proxyType is not a string: {}", value),
                        ))
                    }
                },

                "proxyAutoconfigUrl" => match value.as_str() {
                    Some(x) => {
                        Url::parse(x).map_err(|_| {
                            WebDriverError::new(
                                ErrorStatus::InvalidArgument,
                                format!("proxyAutoconfigUrl is not a valid URL: {}", x),
                            )
                        })?;
                    }
                    None => {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidArgument,
                            "proxyAutoconfigUrl is not a string",
                        ))
                    }
                },

                "httpProxy" => SpecNewSessionParameters::validate_host(value, "httpProxy")?,
                "noProxy" => SpecNewSessionParameters::validate_no_proxy(value)?,
                "sslProxy" => SpecNewSessionParameters::validate_host(value, "sslProxy")?,
                "socksProxy" => SpecNewSessionParameters::validate_host(value, "socksProxy")?,
                "socksVersion" => {
                    if !value.is_number() {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidArgument,
                            format!("socksVersion is not a number: {}", value),
                        ));
                    }
                }

                x => {
                    return Err(WebDriverError::new(
                        ErrorStatus::InvalidArgument,
                        format!("Invalid proxy configuration entry: {}", x),
                    ))
                }
            }
        }

        Ok(())
    }

    fn validate_no_proxy(value: &Value) -> WebDriverResult<()> {
        match value.as_array() {
            Some(hosts) => {
                for host in hosts {
                    match host.as_str() {
                        Some(_) => {}
                        None => {
                            return Err(WebDriverError::new(
                                ErrorStatus::InvalidArgument,
                                format!("noProxy item is not a string: {}", host),
                            ))
                        }
                    }
                }
            }
            None => {
                return Err(WebDriverError::new(
                    ErrorStatus::InvalidArgument,
                    format!("noProxy is not an array: {}", value),
                ))
            }
        }

        Ok(())
    }

    /// Validate whether a named capability is JSON value is a string
    /// containing a host and possible port
    fn validate_host(value: &Value, entry: &str) -> WebDriverResult<()> {
        match value.as_str() {
            Some(host) => {
                if host.contains("://") {
                    return Err(WebDriverError::new(
                        ErrorStatus::InvalidArgument,
                        format!("{} must not contain a scheme: {}", entry, host),
                    ));
                }

                // Temporarily add a scheme so the host can be parsed as URL
                let url = Url::parse(&format!("http://{}", host)).map_err(|_| {
                    WebDriverError::new(
                        ErrorStatus::InvalidArgument,
                        format!("{} is not a valid URL: {}", entry, host),
                    )
                })?;

                if url.username() != ""
                    || url.password().is_some()
                    || url.path() != "/"
                    || url.query().is_some()
                    || url.fragment().is_some()
                {
                    return Err(WebDriverError::new(
                        ErrorStatus::InvalidArgument,
                        format!("{} is not of the form host[:port]: {}", entry, host),
                    ));
                }
            }

            None => {
                return Err(WebDriverError::new(
                    ErrorStatus::InvalidArgument,
                    format!("{} is not a string: {}", entry, value),
                ))
            }
        }

        Ok(())
    }

    fn validate_timeouts(value: &Value) -> WebDriverResult<()> {
        let obj = try_opt!(
            value.as_object(),
            ErrorStatus::InvalidArgument,
            "timeouts capability is not an object"
        );

        for (key, value) in obj {
            match &**key {
                "script" | "pageLoad" | "implicit" if value.is_null() => {}

                x @ "script" | x @ "pageLoad" | x @ "implicit" => {
                    let timeout = try_opt!(
                        value.as_f64(),
                        ErrorStatus::InvalidArgument,
                        format!("{} timeouts value is not a number: {}", x, value)
                    );
                    if timeout < 0.0 || timeout.fract() != 0.0 {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidArgument,
                            format!(
                                "'{}' timeouts value is not a positive Integer: {}",
                                x, timeout
                            ),
                        ));
                    }
                    if (timeout as u64) > MAX_SAFE_INTEGER {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidArgument,
                            format!(
                                "'{}' timeouts value is greater than maximum safe integer: {}",
                                x, timeout
                            ),
                        ));
                    }
                }

                x => {
                    return Err(WebDriverError::new(
                        ErrorStatus::InvalidArgument,
                        format!("Invalid timeouts capability entry: {}", x),
                    ))
                }
            }
        }

        Ok(())
    }

    fn validate_unhandled_prompt_behavior(value: &Value) -> WebDriverResult<()> {
        match value {
            Value::Object(obj) => {
                // Unhandled Prompt Behavior type as used by WebDriver BiDi
                for (key, value) in obj {
                    match &**key {
                        x @ "alert"
                        | x @ "beforeUnload"
                        | x @ "confirm"
                        | x @ "default"
                        | x @ "file"
                        | x @ "prompt" => {
                            let behavior = try_opt!(
                                value.as_str(),
                                ErrorStatus::InvalidArgument,
                                format!(
                                    "'{}' unhandledPromptBehavior value is not a string: {}",
                                    x, value
                                )
                            );

                            match behavior {
                                "accept" | "accept and notify" | "dismiss"
                                | "dismiss and notify" | "ignore" => {}
                                x => {
                                    return Err(WebDriverError::new(
                                        ErrorStatus::InvalidArgument,
                                        format!(
                                            "'{}' unhandledPromptBehavior value is invalid: {}",
                                            x, behavior
                                        ),
                                    ))
                                }
                            }
                        }
                        x => {
                            return Err(WebDriverError::new(
                                ErrorStatus::InvalidArgument,
                                format!("Invalid unhandledPromptBehavior entry: {}", x),
                            ))
                        }
                    }
                }
            }
            Value::String(behavior) => match behavior.as_str() {
                "accept" | "accept and notify" | "dismiss" | "dismiss and notify" | "ignore" => {}
                x => {
                    return Err(WebDriverError::new(
                        ErrorStatus::InvalidArgument,
                        format!("Invalid unhandledPromptBehavior value: {}", x),
                    ))
                }
            },
            _ => {
                return Err(WebDriverError::new(
                    ErrorStatus::InvalidArgument,
                    format!(
                        "unhandledPromptBehavior is neither an object nor a string: {}",
                        value
                    ),
                ))
            }
        }

        Ok(())
    }
}

impl CapabilitiesMatching for SpecNewSessionParameters {
    fn match_browser<T: BrowserCapabilities>(
        &self,
        browser_capabilities: &mut T,
    ) -> WebDriverResult<Option<Capabilities>> {
        let default = vec![Map::new()];
        let capabilities_list = if self.firstMatch.is_empty() {
            &default
        } else {
            &self.firstMatch
        };

        let merged_capabilities = capabilities_list
            .iter()
            .map(|first_match_entry| {
                if first_match_entry
                    .keys()
                    .any(|k| self.alwaysMatch.contains_key(k))
                {
                    return Err(WebDriverError::new(
                        ErrorStatus::InvalidArgument,
                        "firstMatch key shadowed a value in alwaysMatch",
                    ));
                }
                let mut merged = self.alwaysMatch.clone();
                for (key, value) in first_match_entry.clone() {
                    merged.insert(key, value);
                }
                Ok(merged)
            })
            .map(|merged| merged.and_then(|x| self.validate(x, browser_capabilities)))
            .collect::<WebDriverResult<Vec<Capabilities>>>()?;

        let selected = merged_capabilities
            .iter()
            .find(|merged| {
                browser_capabilities.init(merged);

                for (key, value) in merged.iter() {
                    match &**key {
                        "browserName" => {
                            let browserValue = browser_capabilities
                                .browser_name(merged)
                                .ok()
                                .and_then(|x| x);

                            if value.as_str() != browserValue.as_deref() {
                                return false;
                            }
                        }
                        "browserVersion" => {
                            let browserValue = browser_capabilities
                                .browser_version(merged)
                                .ok()
                                .and_then(|x| x);
                            // We already validated this was a string
                            let version_cond = value.as_str().unwrap_or("");
                            if let Some(version) = browserValue {
                                if !browser_capabilities
                                    .compare_browser_version(&version, version_cond)
                                    .unwrap_or(false)
                                {
                                    return false;
                                }
                            } else {
                                return false;
                            }
                        }
                        "platformName" => {
                            let browserValue = browser_capabilities
                                .platform_name(merged)
                                .ok()
                                .and_then(|x| x);
                            if value.as_str() != browserValue.as_deref() {
                                return false;
                            }
                        }
                        "acceptInsecureCerts" => {
                            if value.as_bool().unwrap_or(false)
                                && !browser_capabilities
                                    .accept_insecure_certs(merged)
                                    .unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        "setWindowRect" => {
                            if value.as_bool().unwrap_or(false)
                                && !browser_capabilities
                                    .set_window_rect(merged)
                                    .unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        "strictFileInteractability" => {
                            if value.as_bool().unwrap_or(false)
                                && !browser_capabilities
                                    .strict_file_interactability(merged)
                                    .unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        "proxy" => {
                            let default = Map::new();
                            let proxy = value.as_object().unwrap_or(&default);
                            if !browser_capabilities
                                .accept_proxy(proxy, merged)
                                .unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        "webSocketUrl" => {
                            if value.as_bool().unwrap_or(false)
                                && !browser_capabilities.web_socket_url(merged).unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        "webauthn:virtualAuthenticators" => {
                            if value.as_bool().unwrap_or(false)
                                && !browser_capabilities
                                    .webauthn_virtual_authenticators(merged)
                                    .unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        "webauthn:extension:uvm" => {
                            if value.as_bool().unwrap_or(false)
                                && !browser_capabilities
                                    .webauthn_extension_uvm(merged)
                                    .unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        "webauthn:extension:prf" => {
                            if value.as_bool().unwrap_or(false)
                                && !browser_capabilities
                                    .webauthn_extension_prf(merged)
                                    .unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        "webauthn:extension:largeBlob" => {
                            if value.as_bool().unwrap_or(false)
                                && !browser_capabilities
                                    .webauthn_extension_large_blob(merged)
                                    .unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        "webauthn:extension:credBlob" => {
                            if value.as_bool().unwrap_or(false)
                                && !browser_capabilities
                                    .webauthn_extension_cred_blob(merged)
                                    .unwrap_or(false)
                            {
                                return false;
                            }
                        }
                        name => {
                            if name.contains(':') {
                                if !browser_capabilities
                                    .accept_custom(name, value, merged)
                                    .unwrap_or(false)
                                {
                                    return false;
                                }
                            } else {
                                // Accept the capability
                            }
                        }
                    }
                }

                true
            })
            .cloned();
        Ok(selected)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test::assert_de;
    use serde_json::{self, json};

    #[test]
    fn test_json_spec_new_session_parameters_alwaysMatch_only() {
        let caps = SpecNewSessionParameters {
            alwaysMatch: Capabilities::new(),
            firstMatch: vec![Capabilities::new()],
        };
        assert_de(&caps, json!({"alwaysMatch": {}}));
    }

    #[test]
    fn test_json_spec_new_session_parameters_firstMatch_only() {
        let caps = SpecNewSessionParameters {
            alwaysMatch: Capabilities::new(),
            firstMatch: vec![Capabilities::new()],
        };
        assert_de(&caps, json!({"firstMatch": [{}]}));
    }

    #[test]
    fn test_json_spec_new_session_parameters_alwaysMatch_null() {
        let json = json!({
            "alwaysMatch": null,
            "firstMatch": [{}],
        });
        assert!(serde_json::from_value::<SpecNewSessionParameters>(json).is_err());
    }

    #[test]
    fn test_json_spec_new_session_parameters_firstMatch_null() {
        let json = json!({
            "alwaysMatch": {},
            "firstMatch": null,
        });
        assert!(serde_json::from_value::<SpecNewSessionParameters>(json).is_err());
    }

    #[test]
    fn test_json_spec_new_session_parameters_both_empty() {
        let json = json!({
            "alwaysMatch": {},
            "firstMatch": [{}],
        });
        let caps = SpecNewSessionParameters {
            alwaysMatch: Capabilities::new(),
            firstMatch: vec![Capabilities::new()],
        };

        assert_de(&caps, json);
    }

    #[test]
    fn test_json_spec_new_session_parameters_both_with_capability() {
        let json = json!({
            "alwaysMatch": {"foo": "bar"},
            "firstMatch": [{"foo2": "bar2"}],
        });
        let mut caps = SpecNewSessionParameters {
            alwaysMatch: Capabilities::new(),
            firstMatch: vec![Capabilities::new()],
        };
        caps.alwaysMatch.insert("foo".into(), "bar".into());
        caps.firstMatch[0].insert("foo2".into(), "bar2".into());

        assert_de(&caps, json);
    }

    #[test]
    fn test_validate_unhandled_prompt_behavior() {
        fn validate_prompt_behavior(v: Value) -> WebDriverResult<()> {
            SpecNewSessionParameters::validate_unhandled_prompt_behavior(&v)
        }

        // capability as string
        validate_prompt_behavior(json!("accept")).unwrap();
        validate_prompt_behavior(json!("accept and notify")).unwrap();
        validate_prompt_behavior(json!("dismiss")).unwrap();
        validate_prompt_behavior(json!("dismiss and notify")).unwrap();
        validate_prompt_behavior(json!("ignore")).unwrap();
        assert!(validate_prompt_behavior(json!("foo")).is_err());

        // capability as object
        let types = [
            "alert",
            "beforeUnload",
            "confirm",
            "default",
            "file",
            "prompt",
        ];
        let handlers = [
            "accept",
            "accept and notify",
            "dismiss",
            "dismiss and notify",
            "ignore",
        ];
        for promptType in types {
            assert!(validate_prompt_behavior(json!({promptType: "foo"})).is_err());
            for handler in handlers {
                validate_prompt_behavior(json!({promptType: handler})).unwrap();
            }
        }

        for handler in handlers {
            assert!(validate_prompt_behavior(json!({"foo": handler})).is_err());
        }
    }

    #[test]
    fn test_validate_proxy() {
        fn validate_proxy(v: Value) -> WebDriverResult<()> {
            SpecNewSessionParameters::validate_proxy(&v)
        }

        // proxy hosts
        validate_proxy(json!({"httpProxy":  "127.0.0.1"})).unwrap();
        validate_proxy(json!({"httpProxy": "127.0.0.1:"})).unwrap();
        validate_proxy(json!({"httpProxy": "127.0.0.1:3128"})).unwrap();
        validate_proxy(json!({"httpProxy": "localhost"})).unwrap();
        validate_proxy(json!({"httpProxy": "localhost:3128"})).unwrap();
        validate_proxy(json!({"httpProxy": "[2001:db8::1]"})).unwrap();
        validate_proxy(json!({"httpProxy": "[2001:db8::1]:3128"})).unwrap();
        validate_proxy(json!({"httpProxy": "example.org"})).unwrap();
        validate_proxy(json!({"httpProxy": "example.org:3128"})).unwrap();

        assert!(validate_proxy(json!({"httpProxy": "http://example.org"})).is_err());
        assert!(validate_proxy(json!({"httpProxy": "example.org:-1"})).is_err());
        assert!(validate_proxy(json!({"httpProxy": "2001:db8::1"})).is_err());

        // no proxy for manual proxy type
        validate_proxy(json!({"noProxy": ["foo"]})).unwrap();

        assert!(validate_proxy(json!({"noProxy": "foo"})).is_err());
        assert!(validate_proxy(json!({"noProxy": [42]})).is_err());
    }
}
