/* 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::browser::{Browser, LocalBrowser, RemoteBrowser};
use crate::build;
use crate::capabilities::{FirefoxCapabilities, FirefoxOptions, ProfileType};
use crate::command::{
    AddonInstallParameters, AddonPath, AddonUninstallParameters, GeckoContextParameters,
    GeckoExtensionCommand, GeckoExtensionRoute,
};
use crate::logging;
use marionette_rs::common::{
    Cookie as MarionetteCookie, Date as MarionetteDate, Frame as MarionetteFrame,
    Timeouts as MarionetteTimeouts, WebElement as MarionetteWebElement, Window,
};
use marionette_rs::marionette::AppStatus;
use marionette_rs::message::{Command, Message, MessageId, Request};
use marionette_rs::webdriver::{
    AuthenticatorParameters as MarionetteAuthenticatorParameters,
    AuthenticatorTransport as MarionetteAuthenticatorTransport,
    Command as MarionetteWebDriverCommand, CredentialParameters as MarionetteCredentialParameters,
    Keys as MarionetteKeys, Locator as MarionetteLocator, NewWindow as MarionetteNewWindow,
    PrintMargins as MarionettePrintMargins, PrintOrientation as MarionettePrintOrientation,
    PrintPage as MarionettePrintPage, PrintPageRange as MarionettePrintPageRange,
    PrintParameters as MarionettePrintParameters, ScreenshotOptions, Script as MarionetteScript,
    Selector as MarionetteSelector, SetPermissionDescriptor as MarionetteSetPermissionDescriptor,
    SetPermissionParameters as MarionetteSetPermissionParameters,
    SetPermissionState as MarionetteSetPermissionState, Url as MarionetteUrl,
    UserVerificationParameters as MarionetteUserVerificationParameters,
    WebAuthnProtocol as MarionetteWebAuthnProtocol, WindowRect as MarionetteWindowRect,
};
use mozdevice::AndroidStorageInput;
use serde::de::{self, Deserialize, Deserializer};
use serde::ser::{Serialize, Serializer};
use serde_json::{Map, Value};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::io::prelude::*;
use std::io::Error as IoError;
use std::io::ErrorKind;
use std::io::Result as IoResult;
use std::net::{Shutdown, TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::thread;
use std::time;
use url::{Host, Url};
use webdriver::capabilities::BrowserCapabilities;
use webdriver::command::WebDriverCommand::{
    AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert,
    ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension,
    FindElement, FindElementElement, FindElementElements, FindElements, FindShadowRootElement,
    FindShadowRootElements, FullscreenWindow, Get, GetActiveElement, GetAlertText, GetCSSValue,
    GetComputedLabel, GetComputedRole, GetCookies, GetCurrentUrl, GetElementAttribute,
    GetElementProperty, GetElementRect, GetElementTagName, GetElementText, GetNamedCookie,
    GetPageSource, GetShadowRoot, GetTimeouts, GetTitle, GetWindowHandle, GetWindowHandles,
    GetWindowRect, GoBack, GoForward, IsDisplayed, IsEnabled, IsSelected, MaximizeWindow,
    MinimizeWindow, NewSession, NewWindow, PerformActions, Print, Refresh, ReleaseActions,
    SendAlertText, SetPermission, SetTimeouts, SetWindowRect, Status, SwitchToFrame,
    SwitchToParentFrame, SwitchToWindow, TakeElementScreenshot, TakeScreenshot,
    WebAuthnAddCredential, WebAuthnAddVirtualAuthenticator, WebAuthnGetCredentials,
    WebAuthnRemoveAllCredentials, WebAuthnRemoveCredential, WebAuthnRemoveVirtualAuthenticator,
    WebAuthnSetUserVerified,
};
use webdriver::command::{
    ActionsParameters, AddCookieParameters, AuthenticatorParameters, AuthenticatorTransport,
    GetNamedCookieParameters, GetParameters, JavascriptCommandParameters, LocatorParameters,
    NewSessionParameters, NewWindowParameters, PrintMargins, PrintOrientation, PrintPage,
    PrintPageRange, PrintParameters, SendKeysParameters, SetPermissionDescriptor,
    SetPermissionParameters, SetPermissionState, SwitchToFrameParameters, SwitchToWindowParameters,
    TimeoutsParameters, UserVerificationParameters, WebAuthnProtocol, WindowRectParameters,
};
use webdriver::command::{WebDriverCommand, WebDriverMessage};
use webdriver::common::{
    Cookie, CredentialParameters, Date, FrameId, LocatorStrategy, ShadowRoot, WebElement,
    ELEMENT_KEY, FRAME_KEY, SHADOW_KEY, WINDOW_KEY,
};
use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
use webdriver::response::{
    CloseWindowResponse, CookieResponse, CookiesResponse, ElementRectResponse, NewSessionResponse,
    NewWindowResponse, TimeoutsResponse, ValueResponse, WebDriverResponse, WindowRectResponse,
};
use webdriver::server::{Session, WebDriverHandler};
use webdriver::{capabilities::CapabilitiesMatching, server::SessionTeardownKind};

#[derive(Debug, PartialEq, Deserialize)]
struct MarionetteHandshake {
    #[serde(rename = "marionetteProtocol")]
    protocol: u16,
    #[serde(rename = "applicationType")]
    application_type: String,
}

#[derive(Default)]
pub(crate) struct MarionetteSettings {
    pub(crate) binary: Option<PathBuf>,
    pub(crate) profile_root: Option<PathBuf>,
    pub(crate) connect_existing: bool,
    pub(crate) host: String,
    pub(crate) port: Option<u16>,
    pub(crate) websocket_port: u16,
    pub(crate) allow_hosts: Vec<Host>,
    pub(crate) allow_origins: Vec<Url>,
    pub(crate) system_access: bool,

    /// Brings up the Browser Toolbox when starting Firefox,
    /// letting you debug internals.
    pub(crate) jsdebugger: bool,

    pub(crate) android_storage: AndroidStorageInput,
}

#[derive(Default)]
pub(crate) struct MarionetteHandler {
    connection: Mutex<Option<MarionetteConnection>>,
    settings: MarionetteSettings,
}

impl MarionetteHandler {
    pub(crate) fn new(settings: MarionetteSettings) -> MarionetteHandler {
        MarionetteHandler {
            connection: Mutex::new(None),
            settings,
        }
    }

    fn create_connection(
        &self,
        session_id: Option<String>,
        new_session_parameters: &NewSessionParameters,
    ) -> WebDriverResult<MarionetteConnection> {
        let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref());
        let (capabilities, options) = {
            let mut capabilities = new_session_parameters
                .match_browser(&mut fx_capabilities)?
                .ok_or_else(|| {
                    WebDriverError::new(
                        ErrorStatus::SessionNotCreated,
                        "Unable to find a matching set of capabilities",
                    )
                })?;

            let options = FirefoxOptions::from_capabilities(
                fx_capabilities.chosen_binary.clone(),
                &self.settings,
                &mut capabilities,
            )?;
            (capabilities, options)
        };

        if let Some(l) = options.log.level {
            logging::set_max_level(l);
        }

        let marionette_host = self.settings.host.to_owned();
        let marionette_port = match self.settings.port {
            Some(port) => port,
            None => {
                // If we're launching Firefox Desktop version 95 or later, and there's no port
                // specified, we can pass 0 as the port and later read it back from
                // the profile.
                let can_use_profile: bool = options.android.is_none()
                    && options.profile != ProfileType::Named
                    && !self.settings.connect_existing
                    && fx_capabilities
                        .browser_version(&capabilities)
                        .map(|opt_v| {
                            opt_v
                                .map(|v| {
                                    fx_capabilities
                                        .compare_browser_version(&v, ">=95")
                                        .unwrap_or(false)
                                })
                                .unwrap_or(false)
                        })
                        .unwrap_or(false);
                if can_use_profile {
                    0
                } else {
                    get_free_port(&marionette_host)?
                }
            }
        };

        let websocket_port = if options.use_websocket {
            Some(self.settings.websocket_port)
        } else {
            None
        };

        let browser = if options.android.is_some() {
            // TODO: support connecting to running Apps.  There's no real obstruction here,
            // just some details about port forwarding to work through.  We can't follow
            // `chromedriver` here since it uses an abstract socket rather than a TCP socket:
            // see bug 1240830 for thoughts on doing that for Marionette.
            if self.settings.connect_existing {
                return Err(WebDriverError::new(
                    ErrorStatus::SessionNotCreated,
                    "Cannot connect to an existing Android App yet",
                ));
            }
            Browser::Remote(RemoteBrowser::new(
                options,
                marionette_port,
                websocket_port,
                self.settings.system_access,
                self.settings.profile_root.as_deref(),
            )?)
        } else if !self.settings.connect_existing {
            Browser::Local(LocalBrowser::new(
                options,
                marionette_port,
                self.settings.jsdebugger,
                self.settings.system_access,
                self.settings.profile_root.as_deref(),
            )?)
        } else {
            Browser::Existing(marionette_port)
        };
        let session = MarionetteSession::new(session_id, capabilities);
        MarionetteConnection::new(marionette_host, browser, session)
    }

    fn close_connection(&mut self, wait_for_shutdown: bool) {
        if let Ok(connection) = self.connection.get_mut() {
            if let Some(conn) = connection.take() {
                if let Err(e) = conn.close(wait_for_shutdown) {
                    error!("Failed to close browser connection: {}", e)
                }
            }
        }
    }
}

impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler {
    fn handle_command(
        &mut self,
        _: &Option<Session>,
        msg: WebDriverMessage<GeckoExtensionRoute>,
    ) -> WebDriverResult<WebDriverResponse> {
        // First handle the status message which doesn't actually require a marionette
        // connection or message
        if let Status = msg.command {
            let (ready, message) = self
                .connection
                .get_mut()
                .map(|ref connection| {
                    connection
                        .as_ref()
                        .map(|_| (false, "Session already started"))
                        .unwrap_or((true, ""))
                })
                .unwrap_or((false, "geckodriver internal error"));
            let mut value = Map::new();
            value.insert("ready".to_string(), Value::Bool(ready));
            value.insert("message".to_string(), Value::String(message.into()));
            return Ok(WebDriverResponse::Generic(ValueResponse(Value::Object(
                value,
            ))));
        }

        match self.connection.lock() {
            Ok(mut connection) => {
                if connection.is_none() {
                    if let NewSession(ref capabilities) = msg.command {
                        let conn = self.create_connection(msg.session_id.clone(), capabilities)?;
                        *connection = Some(conn);
                    } else {
                        return Err(WebDriverError::new(
                            ErrorStatus::InvalidSessionId,
                            "Tried to run command without establishing a connection",
                        ));
                    }
                }
                let conn = connection.as_mut().expect("Missing connection");
                conn.send_command(&msg).map_err(|mut err| {
                    // Shutdown the browser if no session can
                    // be established due to errors.
                    if let NewSession(_) = msg.command {
                        err.delete_session = true;
                    }
                    err
                })
            }
            Err(_) => Err(WebDriverError::new(
                ErrorStatus::UnknownError,
                "Failed to aquire Marionette connection",
            )),
        }
    }

    fn teardown_session(&mut self, kind: SessionTeardownKind) {
        let wait_for_shutdown = match kind {
            SessionTeardownKind::Deleted => true,
            SessionTeardownKind::NotDeleted => false,
        };
        self.close_connection(wait_for_shutdown);
    }
}

impl Drop for MarionetteHandler {
    fn drop(&mut self) {
        self.close_connection(false);
    }
}

struct MarionetteSession {
    session_id: String,
    capabilities: Map<String, Value>,
    command_id: MessageId,
}

impl MarionetteSession {
    fn new(session_id: Option<String>, capabilities: Map<String, Value>) -> MarionetteSession {
        let initital_id = session_id.unwrap_or_default();
        MarionetteSession {
            session_id: initital_id,
            capabilities,
            command_id: 0,
        }
    }

    fn update(
        &mut self,
        msg: &WebDriverMessage<GeckoExtensionRoute>,
        resp: &MarionetteResponse,
    ) -> WebDriverResult<()> {
        if let NewSession(_) = msg.command {
            let session_id = try_opt!(
                try_opt!(
                    resp.result.get("sessionId"),
                    ErrorStatus::SessionNotCreated,
                    "Unable to get session id"
                )
                .as_str(),
                ErrorStatus::SessionNotCreated,
                "Unable to convert session id to string"
            );
            self.session_id = session_id.to_string();
        };
        Ok(())
    }

    /// Converts a Marionette JSON response into a `WebElement`.
    ///
    /// Note that it currently coerces all chrome elements, web frames, and web
    /// windows also into web elements.  This will change at a later point.
    fn to_web_element(&self, json_data: &Value) -> WebDriverResult<WebElement> {
        let data = try_opt!(
            json_data.as_object(),
            ErrorStatus::UnknownError,
            "Failed to convert data to an object"
        );

        let element = data.get(ELEMENT_KEY);
        let frame = data.get(FRAME_KEY);
        let window = data.get(WINDOW_KEY);

        let value = try_opt!(
            element.or(frame).or(window),
            ErrorStatus::UnknownError,
            "Failed to extract web element from Marionette response"
        );
        let id = try_opt!(
            value.as_str(),
            ErrorStatus::UnknownError,
            "Failed to convert web element reference value to string"
        )
        .to_string();
        Ok(WebElement(id))
    }

    /// Converts a Marionette JSON response into a `ShadowRoot`.
    fn to_shadow_root(&self, json_data: &Value) -> WebDriverResult<ShadowRoot> {
        let data = try_opt!(
            json_data.as_object(),
            ErrorStatus::UnknownError,
            "Failed to convert data to an object"
        );

        let shadow_root = data.get(SHADOW_KEY);

        let value = try_opt!(
            shadow_root,
            ErrorStatus::UnknownError,
            "Failed to extract shadow root from Marionette response"
        );
        let id = try_opt!(
            value.as_str(),
            ErrorStatus::UnknownError,
            "Failed to convert shadow root reference value to string"
        )
        .to_string();
        Ok(ShadowRoot(id))
    }

    fn next_command_id(&mut self) -> MessageId {
        self.command_id += 1;
        self.command_id
    }

    fn response(
        &mut self,
        msg: &WebDriverMessage<GeckoExtensionRoute>,
        resp: MarionetteResponse,
    ) -> WebDriverResult<WebDriverResponse> {
        use self::GeckoExtensionCommand::*;

        if resp.id != self.command_id {
            return Err(WebDriverError::new(
                ErrorStatus::UnknownError,
                format!(
                    "Marionette responses arrived out of sequence, expected {}, got {}",
                    self.command_id, resp.id
                ),
            ));
        }

        if let Some(error) = resp.error {
            return Err(error.into());
        }

        self.update(msg, &resp)?;

        Ok(match msg.command {
            // Everything that doesn't have a response value
            Get(_)
            | GoBack
            | GoForward
            | Refresh
            | SetTimeouts(_)
            | SwitchToWindow(_)
            | SwitchToFrame(_)
            | SwitchToParentFrame
            | AddCookie(_)
            | DeleteCookies
            | DeleteCookie(_)
            | DismissAlert
            | AcceptAlert
            | SendAlertText(_)
            | ElementClick(_)
            | ElementClear(_)
            | ElementSendKeys(_, _)
            | PerformActions(_)
            | ReleaseActions => WebDriverResponse::Void,
            // Things that simply return the contents of the marionette "value" property
            GetCurrentUrl
            | GetTitle
            | GetPageSource
            | GetWindowHandle
            | IsDisplayed(_)
            | IsSelected(_)
            | GetElementAttribute(_, _)
            | GetElementProperty(_, _)
            | GetCSSValue(_, _)
            | GetElementText(_)
            | GetElementTagName(_)
            | GetComputedLabel(_)
            | GetComputedRole(_)
            | IsEnabled(_)
            | ExecuteScript(_)
            | ExecuteAsyncScript(_)
            | GetAlertText
            | TakeScreenshot
            | Print(_)
            | SetPermission(_)
            | TakeElementScreenshot(_)
            | WebAuthnAddVirtualAuthenticator(_)
            | WebAuthnRemoveVirtualAuthenticator
            | WebAuthnAddCredential(_)
            | WebAuthnGetCredentials
            | WebAuthnRemoveCredential
            | WebAuthnRemoveAllCredentials
            | WebAuthnSetUserVerified(_) => {
                WebDriverResponse::Generic(resp.into_value_response(true)?)
            }
            GetTimeouts => {
                let script = match try_opt!(
                    resp.result.get("script"),
                    ErrorStatus::UnknownError,
                    "Missing field: script"
                ) {
                    Value::Null => None,
                    n => try_opt!(
                        Some(n.as_u64()),
                        ErrorStatus::UnknownError,
                        "Failed to interpret script timeout duration as u64"
                    ),
                };
                let page_load = try_opt!(
                    try_opt!(
                        resp.result.get("pageLoad"),
                        ErrorStatus::UnknownError,
                        "Missing field: pageLoad"
                    )
                    .as_u64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret page load duration as u64"
                );
                let implicit = try_opt!(
                    try_opt!(
                        resp.result.get("implicit"),
                        ErrorStatus::UnknownError,
                        "Missing field: implicit"
                    )
                    .as_u64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret implicit search duration as u64"
                );

                WebDriverResponse::Timeouts(TimeoutsResponse {
                    script,
                    page_load,
                    implicit,
                })
            }
            Status => panic!("Got status command that should already have been handled"),
            GetWindowHandles => WebDriverResponse::Generic(resp.into_value_response(false)?),
            NewWindow(_) => {
                let handle: String = try_opt!(
                    try_opt!(
                        resp.result.get("handle"),
                        ErrorStatus::UnknownError,
                        "Failed to find handle field"
                    )
                    .as_str(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret handle as string"
                )
                .into();
                let typ: String = try_opt!(
                    try_opt!(
                        resp.result.get("type"),
                        ErrorStatus::UnknownError,
                        "Failed to find type field"
                    )
                    .as_str(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret type as string"
                )
                .into();

                WebDriverResponse::NewWindow(NewWindowResponse { handle, typ })
            }
            CloseWindow => {
                let data = try_opt!(
                    resp.result.as_array(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret value as array"
                );
                let handles = data
                    .iter()
                    .map(|x| {
                        Ok(try_opt!(
                            x.as_str(),
                            ErrorStatus::UnknownError,
                            "Failed to interpret window handle as string"
                        )
                        .to_owned())
                    })
                    .collect::<Result<Vec<_>, _>>()?;
                WebDriverResponse::CloseWindow(CloseWindowResponse(handles))
            }
            GetElementRect(_) => {
                let x = try_opt!(
                    try_opt!(
                        resp.result.get("x"),
                        ErrorStatus::UnknownError,
                        "Failed to find x field"
                    )
                    .as_f64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret x as float"
                );

                let y = try_opt!(
                    try_opt!(
                        resp.result.get("y"),
                        ErrorStatus::UnknownError,
                        "Failed to find y field"
                    )
                    .as_f64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret y as float"
                );

                let width = try_opt!(
                    try_opt!(
                        resp.result.get("width"),
                        ErrorStatus::UnknownError,
                        "Failed to find width field"
                    )
                    .as_f64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret width as float"
                );

                let height = try_opt!(
                    try_opt!(
                        resp.result.get("height"),
                        ErrorStatus::UnknownError,
                        "Failed to find height field"
                    )
                    .as_f64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret width as float"
                );

                let rect = ElementRectResponse {
                    x,
                    y,
                    width,
                    height,
                };
                WebDriverResponse::ElementRect(rect)
            }
            FullscreenWindow | MinimizeWindow | MaximizeWindow | GetWindowRect
            | SetWindowRect(_) => {
                let width = try_opt!(
                    try_opt!(
                        resp.result.get("width"),
                        ErrorStatus::UnknownError,
                        "Failed to find width field"
                    )
                    .as_u64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret width as positive integer"
                );

                let height = try_opt!(
                    try_opt!(
                        resp.result.get("height"),
                        ErrorStatus::UnknownError,
                        "Failed to find heigenht field"
                    )
                    .as_u64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret height as positive integer"
                );

                let x = try_opt!(
                    try_opt!(
                        resp.result.get("x"),
                        ErrorStatus::UnknownError,
                        "Failed to find x field"
                    )
                    .as_i64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret x as integer"
                );

                let y = try_opt!(
                    try_opt!(
                        resp.result.get("y"),
                        ErrorStatus::UnknownError,
                        "Failed to find y field"
                    )
                    .as_i64(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret y as integer"
                );

                let rect = WindowRectResponse {
                    x: x as i32,
                    y: y as i32,
                    width: width as i32,
                    height: height as i32,
                };
                WebDriverResponse::WindowRect(rect)
            }
            GetCookies => {
                let cookies: Vec<Cookie> = serde_json::from_value(resp.result)?;
                WebDriverResponse::Cookies(CookiesResponse(cookies))
            }
            GetNamedCookie(ref name) => {
                let mut cookies: Vec<Cookie> = serde_json::from_value(resp.result)?;
                cookies.retain(|x| x.name == *name);
                let cookie = try_opt!(
                    cookies.pop(),
                    ErrorStatus::NoSuchCookie,
                    format!("No cookie with name {}", name)
                );
                WebDriverResponse::Cookie(CookieResponse(cookie))
            }
            FindElement(_) | FindElementElement(_, _) | FindShadowRootElement(_, _) => {
                let element = self.to_web_element(try_opt!(
                    resp.result.get("value"),
                    ErrorStatus::UnknownError,
                    "Failed to find value field"
                ))?;
                WebDriverResponse::Generic(ValueResponse(serde_json::to_value(element)?))
            }
            FindElements(_) | FindElementElements(_, _) | FindShadowRootElements(_, _) => {
                let element_vec = try_opt!(
                    resp.result.as_array(),
                    ErrorStatus::UnknownError,
                    "Failed to interpret value as array"
                );
                let elements = element_vec
                    .iter()
                    .map(|x| self.to_web_element(x))
                    .collect::<Result<Vec<_>, _>>()?;

                // TODO(Henrik): How to remove unwrap?
                WebDriverResponse::Generic(ValueResponse(Value::Array(
                    elements
                        .iter()
                        .map(|x| serde_json::to_value(x).unwrap())
                        .collect(),
                )))
            }
            GetShadowRoot(_) => {
                let shadow_root = self.to_shadow_root(try_opt!(
                    resp.result.get("value"),
                    ErrorStatus::UnknownError,
                    "Failed to find value field"
                ))?;
                WebDriverResponse::Generic(ValueResponse(serde_json::to_value(shadow_root)?))
            }
            GetActiveElement => {
                let element = self.to_web_element(try_opt!(
                    resp.result.get("value"),
                    ErrorStatus::UnknownError,
                    "Failed to find value field"
                ))?;
                WebDriverResponse::Generic(ValueResponse(serde_json::to_value(element)?))
            }
            NewSession(_) => {
                let session_id = try_opt!(
                    try_opt!(
                        resp.result.get("sessionId"),
                        ErrorStatus::InvalidSessionId,
                        "Failed to find sessionId field"
                    )
                    .as_str(),
                    ErrorStatus::InvalidSessionId,
                    "sessionId is not a string"
                );

                let mut capabilities = try_opt!(
                    try_opt!(
                        resp.result.get("capabilities"),
                        ErrorStatus::UnknownError,
                        "Failed to find capabilities field"
                    )
                    .as_object(),
                    ErrorStatus::UnknownError,
                    "capabilities field is not an object"
                )
                .clone();

                capabilities.insert("moz:geckodriverVersion".into(), build::build_info().into());

                WebDriverResponse::NewSession(NewSessionResponse::new(
                    session_id.to_string(),
                    Value::Object(capabilities),
                ))
            }
            DeleteSession => WebDriverResponse::DeleteSession,
            Extension(ref extension) => match extension {
                GetContext => WebDriverResponse::Generic(resp.into_value_response(true)?),
                SetContext(_) => WebDriverResponse::Void,
                InstallAddon(_) => WebDriverResponse::Generic(resp.into_value_response(true)?),
                UninstallAddon(_) => WebDriverResponse::Void,
                TakeFullScreenshot => WebDriverResponse::Generic(resp.into_value_response(true)?),
            },
        })
    }
}

fn try_convert_to_marionette_message(
    msg: &WebDriverMessage<GeckoExtensionRoute>,
    browser: &Browser,
) -> WebDriverResult<Option<Command>> {
    use self::GeckoExtensionCommand::*;
    use self::WebDriverCommand::*;

    Ok(match msg.command {
        AcceptAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::AcceptAlert)),
        AddCookie(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::AddCookie(
            x.to_marionette()?,
        ))),
        CloseWindow => Some(Command::WebDriver(MarionetteWebDriverCommand::CloseWindow)),
        DeleteCookie(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::DeleteCookie(x.clone()),
        )),
        DeleteCookies => Some(Command::WebDriver(
            MarionetteWebDriverCommand::DeleteCookies,
        )),
        DeleteSession => match browser {
            Browser::Local(_) | Browser::Remote(_) => Some(Command::Marionette(
                marionette_rs::marionette::Command::DeleteSession {
                    flags: vec![AppStatus::eForceQuit],
                },
            )),
            Browser::Existing(_) => Some(Command::WebDriver(
                MarionetteWebDriverCommand::DeleteSession,
            )),
        },
        DismissAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::DismissAlert)),
        ElementClear(ref e) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::ElementClear {
                id: e.clone().to_string(),
            },
        )),
        ElementClick(ref e) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::ElementClick {
                id: e.clone().to_string(),
            },
        )),
        ElementSendKeys(ref e, ref x) => {
            let keys = x.to_marionette()?;
            Some(Command::WebDriver(
                MarionetteWebDriverCommand::ElementSendKeys {
                    id: e.clone().to_string(),
                    text: keys.text.clone(),
                    value: keys.value,
                },
            ))
        }
        ExecuteAsyncScript(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::ExecuteAsyncScript(x.to_marionette()?),
        )),
        ExecuteScript(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::ExecuteScript(x.to_marionette()?),
        )),
        FindElement(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::FindElement(
            x.to_marionette()?,
        ))),
        FindElements(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::FindElements(x.to_marionette()?),
        )),
        FindElementElement(ref e, ref x) => {
            let locator = x.to_marionette()?;
            Some(Command::WebDriver(MarionetteWebDriverCommand::FindElement(
                MarionetteLocator {
                    element: Some(e.clone().to_string()),
                    using: locator.using,
                    value: locator.value,
                },
            )))
        }
        FindElementElements(ref e, ref x) => {
            let locator = x.to_marionette()?;
            Some(Command::WebDriver(
                MarionetteWebDriverCommand::FindElements(MarionetteLocator {
                    element: Some(e.clone().to_string()),
                    using: locator.using,
                    value: locator.value,
                }),
            ))
        }
        FindShadowRootElement(ref s, ref x) => {
            let locator = x.to_marionette()?;
            Some(Command::WebDriver(
                MarionetteWebDriverCommand::FindShadowRootElement {
                    shadow_root: s.clone().to_string(),
                    using: locator.using,
                    value: locator.value,
                },
            ))
        }
        FindShadowRootElements(ref s, ref x) => {
            let locator = x.to_marionette()?;
            Some(Command::WebDriver(
                MarionetteWebDriverCommand::FindShadowRootElements {
                    shadow_root: s.clone().to_string(),
                    using: locator.using.clone(),
                    value: locator.value,
                },
            ))
        }
        FullscreenWindow => Some(Command::WebDriver(
            MarionetteWebDriverCommand::FullscreenWindow,
        )),
        Get(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::Get(
            x.to_marionette()?,
        ))),
        GetActiveElement => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetActiveElement,
        )),
        GetAlertText => Some(Command::WebDriver(MarionetteWebDriverCommand::GetAlertText)),
        GetComputedLabel(ref e) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetComputedLabel {
                id: e.clone().to_string(),
            },
        )),
        GetComputedRole(ref e) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetComputedRole {
                id: e.clone().to_string(),
            },
        )),
        GetCookies | GetNamedCookie(_) => {
            Some(Command::WebDriver(MarionetteWebDriverCommand::GetCookies))
        }
        GetCSSValue(ref e, ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetCSSValue {
                id: e.clone().to_string(),
                property: x.clone(),
            },
        )),
        GetCurrentUrl => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetCurrentUrl,
        )),
        GetElementAttribute(ref e, ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetElementAttribute {
                id: e.clone().to_string(),
                name: x.clone(),
            },
        )),
        GetElementProperty(ref e, ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetElementProperty {
                id: e.clone().to_string(),
                name: x.clone(),
            },
        )),
        GetElementRect(ref e) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetElementRect {
                id: e.clone().to_string(),
            },
        )),
        GetElementTagName(ref e) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetElementTagName {
                id: e.clone().to_string(),
            },
        )),
        GetElementText(ref e) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetElementText {
                id: e.clone().to_string(),
            },
        )),
        GetPageSource => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetPageSource,
        )),
        GetShadowRoot(ref e) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetShadowRoot {
                id: e.clone().to_string(),
            },
        )),
        GetTitle => Some(Command::WebDriver(MarionetteWebDriverCommand::GetTitle)),
        GetWindowHandle => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetWindowHandle,
        )),
        GetWindowHandles => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetWindowHandles,
        )),
        GetWindowRect => Some(Command::WebDriver(
            MarionetteWebDriverCommand::GetWindowRect,
        )),
        GetTimeouts => Some(Command::WebDriver(MarionetteWebDriverCommand::GetTimeouts)),
        GoBack => Some(Command::WebDriver(MarionetteWebDriverCommand::GoBack)),
        GoForward => Some(Command::WebDriver(MarionetteWebDriverCommand::GoForward)),
        IsDisplayed(ref e) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::IsDisplayed {
                id: e.clone().to_string(),
            },
        )),
        IsEnabled(ref e) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsEnabled {
            id: e.clone().to_string(),
        })),
        IsSelected(ref e) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsSelected {
            id: e.clone().to_string(),
        })),
        MaximizeWindow => Some(Command::WebDriver(
            MarionetteWebDriverCommand::MaximizeWindow,
        )),
        MinimizeWindow => Some(Command::WebDriver(
            MarionetteWebDriverCommand::MinimizeWindow,
        )),
        NewWindow(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::NewWindow(
            x.to_marionette()?,
        ))),
        Print(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::Print(
            x.to_marionette()?,
        ))),
        WebAuthnAddVirtualAuthenticator(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::WebAuthnAddVirtualAuthenticator(x.to_marionette()?),
        )),
        WebAuthnRemoveVirtualAuthenticator => Some(Command::WebDriver(
            MarionetteWebDriverCommand::WebAuthnRemoveVirtualAuthenticator,
        )),
        WebAuthnAddCredential(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::WebAuthnAddCredential(x.to_marionette()?),
        )),
        WebAuthnGetCredentials => Some(Command::WebDriver(
            MarionetteWebDriverCommand::WebAuthnGetCredentials,
        )),
        WebAuthnRemoveCredential => Some(Command::WebDriver(
            MarionetteWebDriverCommand::WebAuthnRemoveCredential,
        )),
        WebAuthnRemoveAllCredentials => Some(Command::WebDriver(
            MarionetteWebDriverCommand::WebAuthnRemoveAllCredentials,
        )),
        WebAuthnSetUserVerified(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::WebAuthnSetUserVerified(x.to_marionette()?),
        )),
        Refresh => Some(Command::WebDriver(MarionetteWebDriverCommand::Refresh)),
        ReleaseActions => Some(Command::WebDriver(
            MarionetteWebDriverCommand::ReleaseActions,
        )),
        SendAlertText(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::SendAlertText(x.to_marionette()?),
        )),
        SetPermission(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::SetPermission(x.to_marionette()?),
        )),
        SetTimeouts(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::SetTimeouts(
            x.to_marionette()?,
        ))),
        SetWindowRect(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::SetWindowRect(x.to_marionette()?),
        )),
        SwitchToFrame(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::SwitchToFrame(x.to_marionette()?),
        )),
        SwitchToParentFrame => Some(Command::WebDriver(
            MarionetteWebDriverCommand::SwitchToParentFrame,
        )),
        SwitchToWindow(ref x) => Some(Command::WebDriver(
            MarionetteWebDriverCommand::SwitchToWindow(x.to_marionette()?),
        )),
        TakeElementScreenshot(ref e) => {
            let screenshot = ScreenshotOptions {
                id: Some(e.clone().to_string()),
                highlights: vec![],
                full: false,
            };
            Some(Command::WebDriver(
                MarionetteWebDriverCommand::TakeScreenshot(screenshot),
            ))
        }
        TakeScreenshot => {
            let screenshot = ScreenshotOptions {
                id: None,
                highlights: vec![],
                full: false,
            };
            Some(Command::WebDriver(
                MarionetteWebDriverCommand::TakeScreenshot(screenshot),
            ))
        }
        Extension(TakeFullScreenshot) => {
            let screenshot = ScreenshotOptions {
                id: None,
                highlights: vec![],
                full: true,
            };
            Some(Command::WebDriver(
                MarionetteWebDriverCommand::TakeScreenshot(screenshot),
            ))
        }
        _ => None,
    })
}

#[derive(Debug, PartialEq)]
struct MarionetteCommand {
    id: MessageId,
    name: String,
    params: Map<String, Value>,
}

impl Serialize for MarionetteCommand {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let data = (&0, &self.id, &self.name, &self.params);
        data.serialize(serializer)
    }
}

impl MarionetteCommand {
    fn new(id: MessageId, name: String, params: Map<String, Value>) -> MarionetteCommand {
        MarionetteCommand { id, name, params }
    }

    fn encode_msg<T>(msg: T) -> WebDriverResult<String>
    where
        T: serde::Serialize,
    {
        let data = serde_json::to_string(&msg)?;

        Ok(format!("{}:{}", data.len(), data))
    }

    fn from_webdriver_message(
        id: MessageId,
        capabilities: &Map<String, Value>,
        browser: &Browser,
        msg: &WebDriverMessage<GeckoExtensionRoute>,
    ) -> WebDriverResult<String> {
        use self::GeckoExtensionCommand::*;

        if let Some(cmd) = try_convert_to_marionette_message(msg, browser)? {
            let req = Message::Incoming(Request(id, cmd));
            MarionetteCommand::encode_msg(req)
        } else {
            let (opt_name, opt_parameters) = match msg.command {
                Status => panic!("Got status command that should already have been handled"),
                NewSession(_) => {
                    let mut data = Map::new();
                    for (k, v) in capabilities.iter() {
                        data.insert(k.to_string(), serde_json::to_value(v)?);
                    }

                    (Some("WebDriver:NewSession"), Some(Ok(data)))
                }
                PerformActions(ref x) => {
                    (Some("WebDriver:PerformActions"), Some(x.to_marionette()))
                }
                Extension(ref extension) => match extension {
                    GetContext => (Some("Marionette:GetContext"), None),
                    InstallAddon(x) => match x {
                        AddonInstallParameters::AddonBase64(data) => {
                            let addon = AddonPath {
                                path: browser.create_file(&data.addon)?,
                                temporary: data.temporary,
                                allow_private_browsing: data.allow_private_browsing,
                            };
                            (Some("Addon:Install"), Some(addon.to_marionette()))
                        }
                        AddonInstallParameters::AddonPath(data) => {
                            (Some("Addon:Install"), Some(data.to_marionette()))
                        }
                    },
                    SetContext(x) => (Some("Marionette:SetContext"), Some(x.to_marionette())),
                    UninstallAddon(x) => (Some("Addon:Uninstall"), Some(x.to_marionette())),
                    _ => (None, None),
                },
                _ => (None, None),
            };

            let name = try_opt!(
                opt_name,
                ErrorStatus::UnsupportedOperation,
                "Operation not supported"
            );
            let parameters = opt_parameters.unwrap_or_else(|| Ok(Map::new()))?;

            let req = MarionetteCommand::new(id, name.into(), parameters);
            MarionetteCommand::encode_msg(req)
        }
    }
}

#[derive(Debug, PartialEq)]
struct MarionetteResponse {
    id: MessageId,
    error: Option<MarionetteError>,
    result: Value,
}

impl<'de> Deserialize<'de> for MarionetteResponse {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct ResponseWrapper {
            msg_type: u64,
            id: MessageId,
            error: Option<MarionetteError>,
            result: Value,
        }

        let wrapper: ResponseWrapper = Deserialize::deserialize(deserializer)?;

        if wrapper.msg_type != 1 {
            return Err(de::Error::custom(
                "Expected '1' in first element of response",
            ));
        };

        Ok(MarionetteResponse {
            id: wrapper.id,
            error: wrapper.error,
            result: wrapper.result,
        })
    }
}

impl MarionetteResponse {
    fn into_value_response(self, value_required: bool) -> WebDriverResult<ValueResponse> {
        let value: &Value = if value_required {
            try_opt!(
                self.result.get("value"),
                ErrorStatus::UnknownError,
                "Failed to find value field"
            )
        } else {
            &self.result
        };

        Ok(ValueResponse(value.clone()))
    }
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct MarionetteError {
    #[serde(rename = "error")]
    code: String,
    message: String,
    data: Option<BTreeMap<String, Value>>,
    stacktrace: Option<String>,
}

impl From<MarionetteError> for WebDriverError {
    fn from(error: MarionetteError) -> WebDriverError {
        let status = ErrorStatus::from(error.code);
        let message = error.message;

        // Convert `str` to `Cow<'static, str>`
        let data = error
            .data
            .map(|map| map.into_iter().map(|(k, v)| (Cow::Owned(k), v)).collect());

        WebDriverError::new_with_data(status, message, data, error.stacktrace)
    }
}

fn get_free_port(host: &str) -> IoResult<u16> {
    TcpListener::bind((host, 0))
        .and_then(|stream| stream.local_addr())
        .map(|x| x.port())
}

struct MarionetteConnection {
    browser: Browser,
    session: MarionetteSession,
    stream: TcpStream,
}

impl MarionetteConnection {
    fn new(
        host: String,
        mut browser: Browser,
        session: MarionetteSession,
    ) -> WebDriverResult<MarionetteConnection> {
        let stream = match MarionetteConnection::connect(&host, &mut browser) {
            Ok(stream) => stream,
            Err(e) => {
                if let Err(e) = browser.close(true) {
                    error!("Failed to stop browser: {:?}", e);
                }
                return Err(e);
            }
        };
        Ok(MarionetteConnection {
            browser,
            session,
            stream,
        })
    }

    fn connect(host: &str, browser: &mut Browser) -> WebDriverResult<TcpStream> {
        let timeout = time::Duration::from_secs(60);
        let poll_interval = time::Duration::from_millis(100);
        let now = time::Instant::now();

        debug!(
            "Waiting {}s to connect to browser on {}",
            timeout.as_secs(),
            host,
        );

        loop {
            // immediately abort connection attempts if process disappears
            if let Browser::Local(browser) = browser {
                if let Some(status) = browser.check_status() {
                    return Err(WebDriverError::new(
                        ErrorStatus::UnknownError,
                        format!("Process unexpectedly closed with status {}", status),
                    ));
                }
            }

            let last_err;

            if let Some(port) = browser.marionette_port()? {
                match MarionetteConnection::try_connect(host, port) {
                    Ok(stream) => {
                        debug!("Connection to Marionette established on {}:{}.", host, port);
                        browser.update_marionette_port(port);
                        return Ok(stream);
                    }
                    Err(e) => {
                        let err_str = e.to_string();
                        last_err = Some(err_str);
                    }
                }
            } else {
                last_err = Some("Failed to read marionette port".into());
            }
            if now.elapsed() < timeout {
                trace!("Retrying in {:?}", poll_interval);
                thread::sleep(poll_interval);
            } else {
                return Err(WebDriverError::new(
                    ErrorStatus::Timeout,
                    last_err.unwrap_or_else(|| "Unknown error".into()),
                ));
            }
        }
    }

    fn try_connect(host: &str, port: u16) -> WebDriverResult<TcpStream> {
        let mut stream = TcpStream::connect((host, port))?;
        MarionetteConnection::handshake(&mut stream)?;
        Ok(stream)
    }

    fn handshake(stream: &mut TcpStream) -> WebDriverResult<MarionetteHandshake> {
        let resp = (match stream.read_timeout() {
            Ok(timeout) => {
                // If platform supports changing the read timeout of the stream,
                // use a short one only for the handshake with Marionette. Don't
                // make it shorter as 1000ms to not fail on slow connections.
                stream
                    .set_read_timeout(Some(time::Duration::from_millis(1000)))
                    .ok();
                let data = MarionetteConnection::read_resp(stream);
                stream.set_read_timeout(timeout).ok();

                data
            }
            _ => MarionetteConnection::read_resp(stream),
        })
        .map_err(|e| {
            WebDriverError::new(
                ErrorStatus::UnknownError,
                format!("Socket timeout reading Marionette handshake data: {}", e),
            )
        })?;

        let data = serde_json::from_str::<MarionetteHandshake>(&resp)?;

        if data.application_type != "gecko" {
            return Err(WebDriverError::new(
                ErrorStatus::UnknownError,
                format!("Unrecognized application type {}", data.application_type),
            ));
        }

        if data.protocol != 3 {
            return Err(WebDriverError::new(
                ErrorStatus::UnknownError,
                format!(
                    "Unsupported Marionette protocol version {}, required 3",
                    data.protocol
                ),
            ));
        }

        Ok(data)
    }

    fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> {
        // Save minidump files of potential crashes from the profile if requested.
        if let Ok(path) = env::var("MINIDUMP_SAVE_PATH") {
            if let Err(e) = self.save_minidumps(&path) {
                error!(
                    "Failed to save minidump files to the requested location: {}",
                    e
                );
            }
        } else {
            debug!("To store minidump files of Firefox crashes the MINIDUMP_SAVE_PATH environment variable needs to be set.");
        }

        self.stream.shutdown(Shutdown::Both)?;
        self.browser.close(wait_for_shutdown)?;
        Ok(())
    }

    fn save_minidumps(&self, save_path: &str) -> WebDriverResult<()> {
        if !PathBuf::from(&save_path).is_dir() {
            if let Err(e) = fs::create_dir(save_path) {
                warn!(
                    "The specified folder '{}' for minidumps doesn't exist and creation failed: {}",
                    save_path, e
                );

                return Ok(());
            }
        }

        match &self.browser {
            Browser::Local(browser) => {
                if let Some(profile_path) = &browser.profile_path {
                    copy_minidumps_files(profile_path.as_path(), Path::new(&save_path))?;
                }
            }
            Browser::Remote(browser) => {
                browser.handler.copy_minidumps_files(save_path)?;
            }
            Browser::Existing(_) => return Ok(()),
        }

        Ok(())
    }

    fn send_command(
        &mut self,
        msg: &WebDriverMessage<GeckoExtensionRoute>,
    ) -> WebDriverResult<WebDriverResponse> {
        let id = self.session.next_command_id();
        let enc_cmd = MarionetteCommand::from_webdriver_message(
            id,
            &self.session.capabilities,
            &self.browser,
            msg,
        )?;
        let resp_data = self.send(enc_cmd)?;
        let data: MarionetteResponse = serde_json::from_str(&resp_data)?;

        self.session.response(msg, data)
    }

    fn send(&mut self, data: String) -> WebDriverResult<String> {
        if self.stream.write(data.as_bytes()).is_err() {
            let mut err = WebDriverError::new(
                ErrorStatus::UnknownError,
                "Failed to write request to stream",
            );
            err.delete_session = true;
            return Err(err);
        }

        match MarionetteConnection::read_resp(&mut self.stream) {
            Ok(resp) => Ok(resp),
            Err(_) => {
                let mut err = WebDriverError::new(
                    ErrorStatus::UnknownError,
                    "Failed to decode response from marionette",
                );
                err.delete_session = true;
                Err(err)
            }
        }
    }

    fn read_resp(stream: &mut TcpStream) -> IoResult<String> {
        let mut bytes = 0usize;

        loop {
            let buf = &mut [0u8];
            let num_read = stream.read(buf)?;
            let byte = match num_read {
                0 => {
                    return Err(IoError::new(
                        ErrorKind::Other,
                        "EOF reading marionette message",
                    ))
                }
                1 => buf[0],
                _ => panic!("Expected one byte got more"),
            } as char;
            match byte {
                '0'..='9' => {
                    bytes *= 10;
                    bytes += byte as usize - '0' as usize;
                }
                ':' => break,
                _ => {}
            }
        }

        let buf = &mut [0u8; 8192];
        let mut payload = Vec::with_capacity(bytes);
        let mut total_read = 0;
        while total_read < bytes {
            let num_read = stream.read(buf)?;
            if num_read == 0 {
                return Err(IoError::new(
                    ErrorKind::Other,
                    "EOF reading marionette message",
                ));
            }
            total_read += num_read;
            for x in &buf[..num_read] {
                payload.push(*x);
            }
        }

        // TODO(jgraham): Need to handle the error here
        Ok(String::from_utf8(payload).unwrap())
    }
}

fn copy_minidumps_files(profile_path: &Path, save_path: &Path) -> WebDriverResult<()> {
    let mut minidumps_path = profile_path.to_path_buf();
    minidumps_path.push("minidumps");

    // Check if the folder exists and not empty.
    if !minidumps_path.exists() || minidumps_path.read_dir()?.next().is_none() {
        return Ok(());
    }

    match std::fs::read_dir(&minidumps_path) {
        Ok(entries) => {
            for result_entry in entries {
                let entry = result_entry?;
                let file_type = entry.file_type()?;

                if file_type.is_dir() {
                    continue;
                }

                let path = entry.path();
                let extension = path
                    .extension()
                    .and_then(|ext| ext.to_str())
                    .map(|ext| ext.to_lowercase())
                    .unwrap_or(String::from(""));

                // Copy only *.dmp and *.extra files.
                if extension == "dmp" || extension == "extra" {
                    let dest_path = save_path.join(entry.file_name());
                    fs::copy(path, &dest_path)?;

                    debug!(
                        "Copied minidump file {:?} to {:?}.",
                        entry.file_name(),
                        save_path.display()
                    );
                }
            }
        }
        Err(_) => {
            warn!(
                "Couldn't read files from minidumps folder '{}'",
                minidumps_path.display(),
            );

            return Ok(());
        }
    }

    Ok(())
}

trait ToMarionette<T> {
    fn to_marionette(&self) -> WebDriverResult<T>;
}

impl ToMarionette<Map<String, Value>> for AddonPath {
    fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
        let mut data = Map::new();
        data.insert("path".to_string(), serde_json::to_value(&self.path)?);
        if self.temporary.is_some() {
            data.insert(
                "temporary".to_string(),
                serde_json::to_value(self.temporary)?,
            );
        }
        if self.allow_private_browsing.is_some() {
            data.insert(
                "allowPrivateBrowsing".to_string(),
                serde_json::to_value(self.allow_private_browsing)?,
            );
        }
        Ok(data)
    }
}

impl ToMarionette<Map<String, Value>> for AddonUninstallParameters {
    fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
        let mut data = Map::new();
        data.insert("id".to_string(), Value::String(self.id.clone()));
        Ok(data)
    }
}

impl ToMarionette<Map<String, Value>> for GeckoContextParameters {
    fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
        let mut data = Map::new();
        data.insert(
            "value".to_owned(),
            serde_json::to_value(self.context.clone())?,
        );
        Ok(data)
    }
}

impl ToMarionette<MarionettePrintParameters> for PrintParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionettePrintParameters> {
        Ok(MarionettePrintParameters {
            orientation: self.orientation.to_marionette()?,
            scale: self.scale,
            background: self.background,
            page: self.page.to_marionette()?,
            margin: self.margin.to_marionette()?,
            page_ranges: self
                .page_ranges
                .iter()
                .map(|x| x.to_marionette())
                .collect::<WebDriverResult<Vec<_>>>()?,
            shrink_to_fit: self.shrink_to_fit,
        })
    }
}

impl ToMarionette<MarionettePrintOrientation> for PrintOrientation {
    fn to_marionette(&self) -> WebDriverResult<MarionettePrintOrientation> {
        Ok(match self {
            PrintOrientation::Landscape => MarionettePrintOrientation::Landscape,
            PrintOrientation::Portrait => MarionettePrintOrientation::Portrait,
        })
    }
}

impl ToMarionette<MarionettePrintPage> for PrintPage {
    fn to_marionette(&self) -> WebDriverResult<MarionettePrintPage> {
        Ok(MarionettePrintPage {
            width: self.width,
            height: self.height,
        })
    }
}

impl ToMarionette<MarionettePrintPageRange> for PrintPageRange {
    fn to_marionette(&self) -> WebDriverResult<MarionettePrintPageRange> {
        Ok(match self {
            PrintPageRange::Integer(num) => MarionettePrintPageRange::Integer(*num),
            PrintPageRange::Range(range) => MarionettePrintPageRange::Range(range.clone()),
        })
    }
}

impl ToMarionette<MarionettePrintMargins> for PrintMargins {
    fn to_marionette(&self) -> WebDriverResult<MarionettePrintMargins> {
        Ok(MarionettePrintMargins {
            top: self.top,
            bottom: self.bottom,
            left: self.left,
            right: self.right,
        })
    }
}

impl ToMarionette<MarionetteSetPermissionParameters> for SetPermissionParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteSetPermissionParameters> {
        Ok(MarionetteSetPermissionParameters {
            descriptor: self.descriptor.to_marionette()?,
            state: self.state.to_marionette()?,
        })
    }
}

impl ToMarionette<MarionetteSetPermissionDescriptor> for SetPermissionDescriptor {
    fn to_marionette(&self) -> WebDriverResult<MarionetteSetPermissionDescriptor> {
        Ok(MarionetteSetPermissionDescriptor {
            name: self.name.clone(),
        })
    }
}

impl ToMarionette<MarionetteSetPermissionState> for SetPermissionState {
    fn to_marionette(&self) -> WebDriverResult<MarionetteSetPermissionState> {
        Ok(match self {
            SetPermissionState::Denied => MarionetteSetPermissionState::Denied,
            SetPermissionState::Granted => MarionetteSetPermissionState::Granted,
            SetPermissionState::Prompt => MarionetteSetPermissionState::Prompt,
        })
    }
}

impl ToMarionette<MarionetteAuthenticatorParameters> for AuthenticatorParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteAuthenticatorParameters> {
        Ok(MarionetteAuthenticatorParameters {
            protocol: self.protocol.to_marionette()?,
            transport: self.transport.to_marionette()?,
            has_resident_key: self.has_resident_key,
            has_user_verification: self.has_user_verification,
            is_user_consenting: self.is_user_consenting,
            is_user_verified: self.is_user_verified,
        })
    }
}

impl ToMarionette<MarionetteAuthenticatorTransport> for AuthenticatorTransport {
    fn to_marionette(&self) -> WebDriverResult<MarionetteAuthenticatorTransport> {
        Ok(match self {
            AuthenticatorTransport::Usb => MarionetteAuthenticatorTransport::Usb,
            AuthenticatorTransport::Nfc => MarionetteAuthenticatorTransport::Nfc,
            AuthenticatorTransport::Ble => MarionetteAuthenticatorTransport::Ble,
            AuthenticatorTransport::SmartCard => MarionetteAuthenticatorTransport::SmartCard,
            AuthenticatorTransport::Hybrid => MarionetteAuthenticatorTransport::Hybrid,
            AuthenticatorTransport::Internal => MarionetteAuthenticatorTransport::Internal,
        })
    }
}

impl ToMarionette<MarionetteCredentialParameters> for CredentialParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteCredentialParameters> {
        Ok(MarionetteCredentialParameters {
            credential_id: self.credential_id.clone(),
            is_resident_credential: self.is_resident_credential,
            rp_id: self.rp_id.clone(),
            private_key: self.private_key.clone(),
            user_handle: self.user_handle.clone(),
            sign_count: self.sign_count,
        })
    }
}

impl ToMarionette<MarionetteUserVerificationParameters> for UserVerificationParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteUserVerificationParameters> {
        Ok(MarionetteUserVerificationParameters {
            is_user_verified: self.is_user_verified,
        })
    }
}

impl ToMarionette<MarionetteWebAuthnProtocol> for WebAuthnProtocol {
    fn to_marionette(&self) -> WebDriverResult<MarionetteWebAuthnProtocol> {
        Ok(match self {
            WebAuthnProtocol::Ctap1U2f => MarionetteWebAuthnProtocol::Ctap1U2f,
            WebAuthnProtocol::Ctap2 => MarionetteWebAuthnProtocol::Ctap2,
            WebAuthnProtocol::Ctap2_1 => MarionetteWebAuthnProtocol::Ctap2_1,
        })
    }
}

impl ToMarionette<Map<String, Value>> for ActionsParameters {
    fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
        Ok(try_opt!(
            serde_json::to_value(self)?.as_object(),
            ErrorStatus::UnknownError,
            "Expected an object"
        )
        .clone())
    }
}

impl ToMarionette<MarionetteCookie> for AddCookieParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteCookie> {
        Ok(MarionetteCookie {
            name: self.name.clone(),
            value: self.value.clone(),
            path: self.path.clone(),
            domain: self.domain.clone(),
            secure: self.secure,
            http_only: self.httpOnly,
            expiry: match &self.expiry {
                Some(date) => Some(date.to_marionette()?),
                None => None,
            },
            same_site: self.sameSite.clone(),
        })
    }
}

impl ToMarionette<MarionetteDate> for Date {
    fn to_marionette(&self) -> WebDriverResult<MarionetteDate> {
        Ok(MarionetteDate(self.0))
    }
}

impl ToMarionette<Map<String, Value>> for GetNamedCookieParameters {
    fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
        Ok(try_opt!(
            serde_json::to_value(self)?.as_object(),
            ErrorStatus::UnknownError,
            "Expected an object"
        )
        .clone())
    }
}

impl ToMarionette<MarionetteUrl> for GetParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteUrl> {
        Ok(MarionetteUrl {
            url: self.url.clone(),
        })
    }
}

impl ToMarionette<MarionetteScript> for JavascriptCommandParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteScript> {
        Ok(MarionetteScript {
            script: self.script.clone(),
            args: self.args.clone(),
        })
    }
}

impl ToMarionette<MarionetteLocator> for LocatorParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteLocator> {
        Ok(MarionetteLocator {
            element: None,
            using: self.using.to_marionette()?,
            value: self.value.clone(),
        })
    }
}

impl ToMarionette<MarionetteSelector> for LocatorStrategy {
    fn to_marionette(&self) -> WebDriverResult<MarionetteSelector> {
        use self::LocatorStrategy::*;
        match self {
            CSSSelector => Ok(MarionetteSelector::Css),
            LinkText => Ok(MarionetteSelector::LinkText),
            PartialLinkText => Ok(MarionetteSelector::PartialLinkText),
            TagName => Ok(MarionetteSelector::TagName),
            XPath => Ok(MarionetteSelector::XPath),
        }
    }
}

impl ToMarionette<MarionetteNewWindow> for NewWindowParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteNewWindow> {
        Ok(MarionetteNewWindow {
            type_hint: self.type_hint.clone(),
        })
    }
}

impl ToMarionette<MarionetteKeys> for SendKeysParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteKeys> {
        Ok(MarionetteKeys {
            text: self.text.clone(),
            value: self
                .text
                .chars()
                .map(|x| x.to_string())
                .collect::<Vec<String>>(),
        })
    }
}

impl ToMarionette<MarionetteFrame> for SwitchToFrameParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteFrame> {
        Ok(match &self.id {
            FrameId::Short(n) => MarionetteFrame::Index(*n),
            FrameId::Element(el) => MarionetteFrame::Element(el.0.clone()),
            FrameId::Top => MarionetteFrame::Top,
        })
    }
}

impl ToMarionette<Window> for SwitchToWindowParameters {
    fn to_marionette(&self) -> WebDriverResult<Window> {
        Ok(Window {
            handle: self.handle.clone(),
        })
    }
}

impl ToMarionette<MarionetteTimeouts> for TimeoutsParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteTimeouts> {
        Ok(MarionetteTimeouts {
            implicit: self.implicit,
            page_load: self.page_load,
            script: self.script,
        })
    }
}

impl ToMarionette<MarionetteWebElement> for WebElement {
    fn to_marionette(&self) -> WebDriverResult<MarionetteWebElement> {
        Ok(MarionetteWebElement {
            element: self.to_string(),
        })
    }
}

impl ToMarionette<MarionetteWindowRect> for WindowRectParameters {
    fn to_marionette(&self) -> WebDriverResult<MarionetteWindowRect> {
        Ok(MarionetteWindowRect {
            x: self.x,
            y: self.y,
            width: self.width,
            height: self.height,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::path::Path;
    use tempfile::TempDir;

    fn assert_minidump_files(minidumps_path: &Path, filename: &str) {
        let mut dmp_file_present = false;
        let mut extra_file_present = false;

        for result_entry in std::fs::read_dir(minidumps_path).unwrap() {
            let entry = result_entry.unwrap();

            let path: PathBuf = entry.path();
            let filename_from_path = path.file_stem().unwrap().to_str().unwrap();
            if filename == filename_from_path {
                let extension = path.extension().and_then(|ext| ext.to_str()).unwrap();

                if extension == "dmp" {
                    dmp_file_present = true;
                }

                if extension == "extra" {
                    extra_file_present = true;
                }
            }
        }

        assert!(dmp_file_present);
        assert!(extra_file_present);
    }

    fn create_file(folder: &Path, filename: &str) {
        let file = folder.join(filename);
        File::create(&file).unwrap();
    }

    fn create_minidump_files(profile_path: &Path, filename: &str) {
        let folder = create_minidump_folder(profile_path);

        let mut file_extensions = [".dmp", ".extra"];
        for file_extension in file_extensions.iter_mut() {
            let mut filename_with_extension: String = filename.to_owned();
            filename_with_extension.push_str(file_extension);

            create_file(&folder, &filename_with_extension);
        }
    }

    fn create_minidump_folder(profile_path: &Path) -> PathBuf {
        let minidumps_folder = profile_path.join("minidumps");
        if !minidumps_folder.is_dir() {
            fs::create_dir(&minidumps_folder).unwrap();
        }

        minidumps_folder
    }

    #[test]
    fn test_copy_minidumps() {
        let tmp_dir_profile = TempDir::new().unwrap();
        let profile_path = tmp_dir_profile.path();

        let filename = "test";

        create_minidump_files(profile_path, filename);

        let tmp_dir_minidumps = TempDir::new().unwrap();
        let minidumps_path = tmp_dir_minidumps.path();

        copy_minidumps_files(profile_path, minidumps_path).unwrap();

        assert_minidump_files(minidumps_path, filename);

        tmp_dir_profile.close().unwrap();
        tmp_dir_minidumps.close().unwrap();
    }

    #[test]
    fn test_copy_multiple_minidumps() {
        let tmp_dir_profile = TempDir::new().unwrap();
        let profile_path = tmp_dir_profile.path();

        let filename_1 = "test_1";
        create_minidump_files(profile_path, filename_1);

        let filename_2 = "test_2";
        create_minidump_files(profile_path, filename_2);

        let tmp_dir_minidumps = TempDir::new().unwrap();
        let minidumps_path = tmp_dir_minidumps.path();

        copy_minidumps_files(profile_path, minidumps_path).unwrap();

        assert_minidump_files(minidumps_path, filename_1);
        assert_minidump_files(minidumps_path, filename_1);

        tmp_dir_profile.close().unwrap();
        tmp_dir_minidumps.close().unwrap();
    }

    #[test]
    fn test_copy_minidumps_with_non_existent_manifest_path() {
        let tmp_dir_profile = TempDir::new().unwrap();
        let profile_path = tmp_dir_profile.path();

        create_minidump_folder(profile_path);

        assert!(copy_minidumps_files(profile_path, Path::new("/non-existent")).is_ok());

        tmp_dir_profile.close().unwrap();
    }

    #[test]
    fn test_copy_minidumps_with_non_existent_profile_path() {
        let tmp_dir_minidumps = TempDir::new().unwrap();
        let minidumps_path = tmp_dir_minidumps.path();

        assert!(copy_minidumps_files(Path::new("/non-existent"), minidumps_path).is_ok());

        tmp_dir_minidumps.close().unwrap();
    }

    #[test]
    fn test_copy_minidumps_with_non_minidumps_files() {
        let tmp_dir_profile = TempDir::new().unwrap();
        let profile_path = tmp_dir_profile.path();

        let minidumps_folder = create_minidump_folder(profile_path);

        // Create a folder.
        let test_folder_binding = profile_path.join("test");
        let test_folder = test_folder_binding.as_path();
        fs::create_dir(test_folder).unwrap();

        // Create a file with non minidumps extension.
        create_file(&minidumps_folder, "test.txt");

        let tmp_dir_minidumps = TempDir::new().unwrap();
        let minidumps_path = tmp_dir_minidumps.path();

        copy_minidumps_files(profile_path, minidumps_path).unwrap();

        // Check that the non minidump file and the folder were not copied.
        assert!(minidumps_path.read_dir().unwrap().next().is_none());

        tmp_dir_profile.close().unwrap();
        tmp_dir_minidumps.close().unwrap();
    }
}
