/* 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 base64::DecodeError;
use http::StatusCode;
use serde::de::{Deserialize, Deserializer};
use serde::ser::{Serialize, Serializer};
use serde_json::Value;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::error;
use std::io;
use thiserror::Error;

#[derive(Debug, PartialEq)]
pub enum ErrorStatus {
    /// The [element]'s [ShadowRoot] is not attached to the active document,
    /// or the reference is stale
    /// [element]: ../common/struct.WebElement.html
    /// [ShadowRoot]: ../common/struct.ShadowRoot.html
    DetachedShadowRoot,

    /// The [`ElementClick`] command could not be completed because the
    /// [element] receiving the events is obscuring the element that was
    /// requested clicked.
    ///
    /// [`ElementClick`]:
    /// ../command/enum.WebDriverCommand.html#variant.ElementClick
    /// [element]: ../common/struct.WebElement.html
    ElementClickIntercepted,

    /// A [command] could not be completed because the element is not pointer-
    /// or keyboard interactable.
    ///
    /// [command]: ../command/index.html
    ElementNotInteractable,

    /// An attempt was made to select an [element] that cannot be selected.
    ///
    /// [element]: ../common/struct.WebElement.html
    ElementNotSelectable,

    /// Navigation caused the user agent to hit a certificate warning, which is
    /// usually the result of an expired or invalid TLS certificate.
    InsecureCertificate,

    /// The arguments passed to a [command] are either invalid or malformed.
    ///
    /// [command]: ../command/index.html
    InvalidArgument,

    /// An illegal attempt was made to set a cookie under a different domain
    /// than the current page.
    InvalidCookieDomain,

    /// The coordinates provided to an interactions operation are invalid.
    InvalidCoordinates,

    /// A [command] could not be completed because the element is an invalid
    /// state, e.g. attempting to click an element that is no longer attached
    /// to the document.
    ///
    /// [command]: ../command/index.html
    InvalidElementState,

    /// Argument was an invalid selector.
    InvalidSelector,

    /// Occurs if the given session ID is not in the list of active sessions,
    /// meaning the session either does not exist or that it’s not active.
    InvalidSessionId,

    /// An error occurred while executing JavaScript supplied by the user.
    JavascriptError,

    /// The target for mouse interaction is not in the browser’s viewport and
    /// cannot be brought into that viewport.
    MoveTargetOutOfBounds,

    /// An attempt was made to operate on a modal dialogue when one was not
    /// open.
    NoSuchAlert,

    /// No cookie matching the given path name was found amongst the associated
    /// cookies of the current browsing context’s active document.
    NoSuchCookie,

    /// An [element] could not be located on the page using the given search
    /// parameters.
    ///
    /// [element]: ../common/struct.WebElement.html
    NoSuchElement,

    /// A [command] to switch to a frame could not be satisfied because the
    /// frame could not be found.
    ///
    /// [command]: ../command/index.html
    NoSuchFrame,

    /// An [element]'s [ShadowRoot] was not found attached to the element.
    ///
    /// [element]: ../common/struct.WebElement.html
    /// [ShadowRoot]: ../common/struct.ShadowRoot.html
    NoSuchShadowRoot,

    /// A [command] to switch to a window could not be satisfied because the
    /// window could not be found.
    ///
    /// [command]: ../command/index.html
    NoSuchWindow,

    /// A script did not complete before its timeout expired.
    ScriptTimeout,

    /// A new session could not be created.
    SessionNotCreated,

    /// A [command] failed because the referenced [element] is no longer
    /// attached to the DOM.
    ///
    /// [command]: ../command/index.html
    /// [element]: ../common/struct.WebElement.html
    StaleElementReference,

    /// An operation did not complete before its timeout expired.
    Timeout,

    /// A screen capture was made impossible.
    UnableToCaptureScreen,

    /// Setting the cookie’s value could not be done.
    UnableToSetCookie,

    /// A modal dialogue was open, blocking this operation.
    UnexpectedAlertOpen,

    /// The requested command could not be executed because it does not exist.
    UnknownCommand,

    /// An unknown error occurred in the remote end whilst processing the
    /// [command].
    ///
    /// [command]: ../command/index.html
    UnknownError,

    /// The requested [command] matched a known endpoint, but did not match a
    /// method for that endpoint.
    ///
    /// [command]: ../command/index.html
    UnknownMethod,

    /// Indicates that a [command] that should have executed properly is not
    /// currently supported.
    UnsupportedOperation,
}

impl Serialize for ErrorStatus {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        self.error_code().serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for ErrorStatus {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let error_string = String::deserialize(deserializer)?;
        Ok(ErrorStatus::from(error_string))
    }
}

impl ErrorStatus {
    /// Returns the string serialisation of the error type.
    pub fn error_code(&self) -> &'static str {
        use self::ErrorStatus::*;
        match *self {
            DetachedShadowRoot => "detached shadow root",
            ElementClickIntercepted => "element click intercepted",
            ElementNotInteractable => "element not interactable",
            ElementNotSelectable => "element not selectable",
            InsecureCertificate => "insecure certificate",
            InvalidArgument => "invalid argument",
            InvalidCookieDomain => "invalid cookie domain",
            InvalidCoordinates => "invalid coordinates",
            InvalidElementState => "invalid element state",
            InvalidSelector => "invalid selector",
            InvalidSessionId => "invalid session id",
            JavascriptError => "javascript error",
            MoveTargetOutOfBounds => "move target out of bounds",
            NoSuchAlert => "no such alert",
            NoSuchCookie => "no such cookie",
            NoSuchElement => "no such element",
            NoSuchFrame => "no such frame",
            NoSuchShadowRoot => "no such shadow root",
            NoSuchWindow => "no such window",
            ScriptTimeout => "script timeout",
            SessionNotCreated => "session not created",
            StaleElementReference => "stale element reference",
            Timeout => "timeout",
            UnableToCaptureScreen => "unable to capture screen",
            UnableToSetCookie => "unable to set cookie",
            UnexpectedAlertOpen => "unexpected alert open",
            UnknownError => "unknown error",
            UnknownMethod => "unknown method",
            UnknownCommand => "unknown command",
            UnsupportedOperation => "unsupported operation",
        }
    }

    /// Returns the correct HTTP status code associated with the error type.
    pub fn http_status(&self) -> StatusCode {
        use self::ErrorStatus::*;
        match *self {
            DetachedShadowRoot => StatusCode::NOT_FOUND,
            ElementClickIntercepted => StatusCode::BAD_REQUEST,
            ElementNotInteractable => StatusCode::BAD_REQUEST,
            ElementNotSelectable => StatusCode::BAD_REQUEST,
            InsecureCertificate => StatusCode::BAD_REQUEST,
            InvalidArgument => StatusCode::BAD_REQUEST,
            InvalidCookieDomain => StatusCode::BAD_REQUEST,
            InvalidCoordinates => StatusCode::BAD_REQUEST,
            InvalidElementState => StatusCode::BAD_REQUEST,
            InvalidSelector => StatusCode::BAD_REQUEST,
            InvalidSessionId => StatusCode::NOT_FOUND,
            JavascriptError => StatusCode::INTERNAL_SERVER_ERROR,
            MoveTargetOutOfBounds => StatusCode::INTERNAL_SERVER_ERROR,
            NoSuchAlert => StatusCode::NOT_FOUND,
            NoSuchCookie => StatusCode::NOT_FOUND,
            NoSuchElement => StatusCode::NOT_FOUND,
            NoSuchFrame => StatusCode::NOT_FOUND,
            NoSuchShadowRoot => StatusCode::NOT_FOUND,
            NoSuchWindow => StatusCode::NOT_FOUND,
            ScriptTimeout => StatusCode::INTERNAL_SERVER_ERROR,
            SessionNotCreated => StatusCode::INTERNAL_SERVER_ERROR,
            StaleElementReference => StatusCode::NOT_FOUND,
            Timeout => StatusCode::INTERNAL_SERVER_ERROR,
            UnableToCaptureScreen => StatusCode::BAD_REQUEST,
            UnableToSetCookie => StatusCode::INTERNAL_SERVER_ERROR,
            UnexpectedAlertOpen => StatusCode::INTERNAL_SERVER_ERROR,
            UnknownCommand => StatusCode::NOT_FOUND,
            UnknownError => StatusCode::INTERNAL_SERVER_ERROR,
            UnknownMethod => StatusCode::METHOD_NOT_ALLOWED,
            UnsupportedOperation => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

/// Deserialises error type from string.
impl From<String> for ErrorStatus {
    fn from(s: String) -> ErrorStatus {
        use self::ErrorStatus::*;
        match &*s {
            "detached shadow root" => DetachedShadowRoot,
            "element click intercepted" => ElementClickIntercepted,
            "element not interactable" | "element not visible" => ElementNotInteractable,
            "element not selectable" => ElementNotSelectable,
            "insecure certificate" => InsecureCertificate,
            "invalid argument" => InvalidArgument,
            "invalid cookie domain" => InvalidCookieDomain,
            "invalid coordinates" | "invalid element coordinates" => InvalidCoordinates,
            "invalid element state" => InvalidElementState,
            "invalid selector" => InvalidSelector,
            "invalid session id" => InvalidSessionId,
            "javascript error" => JavascriptError,
            "move target out of bounds" => MoveTargetOutOfBounds,
            "no such alert" => NoSuchAlert,
            "no such element" => NoSuchElement,
            "no such frame" => NoSuchFrame,
            "no such shadow root" => NoSuchShadowRoot,
            "no such window" => NoSuchWindow,
            "script timeout" => ScriptTimeout,
            "session not created" => SessionNotCreated,
            "stale element reference" => StaleElementReference,
            "timeout" => Timeout,
            "unable to capture screen" => UnableToCaptureScreen,
            "unable to set cookie" => UnableToSetCookie,
            "unexpected alert open" => UnexpectedAlertOpen,
            "unknown command" => UnknownCommand,
            "unknown error" => UnknownError,
            "unsupported operation" => UnsupportedOperation,
            _ => UnknownError,
        }
    }
}

pub type WebDriverResult<T> = Result<T, WebDriverError>;

#[derive(Debug, PartialEq, Serialize, Error)]
#[serde(remote = "Self")]
#[error("{}", .error.error_code())]
pub struct WebDriverError {
    pub error: ErrorStatus,
    pub message: Cow<'static, str>,
    #[serde(rename = "stacktrace")]
    pub stack: Cow<'static, str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<BTreeMap<Cow<'static, str>, Value>>,
    #[serde(skip)]
    pub delete_session: bool,
}

impl Serialize for WebDriverError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        #[derive(Serialize)]
        struct Wrapper<'a> {
            #[serde(with = "WebDriverError")]
            value: &'a WebDriverError,
        }

        Wrapper { value: self }.serialize(serializer)
    }
}

impl WebDriverError {
    pub fn new<S>(error: ErrorStatus, message: S) -> WebDriverError
    where
        S: Into<Cow<'static, str>>,
    {
        WebDriverError {
            error,
            message: message.into(),
            data: None,
            stack: "".into(),
            delete_session: false,
        }
    }

    pub fn new_with_data<S>(
        error: ErrorStatus,
        message: S,
        data: Option<BTreeMap<Cow<'static, str>, Value>>,
        stacktrace: Option<S>,
    ) -> WebDriverError
    where
        S: Into<Cow<'static, str>>,
    {
        WebDriverError {
            error,
            message: message.into(),
            data,
            stack: stacktrace.map_or_else(|| "".into(), Into::into),
            delete_session: false,
        }
    }

    pub fn error_code(&self) -> &'static str {
        self.error.error_code()
    }

    pub fn http_status(&self) -> StatusCode {
        self.error.http_status()
    }
}

impl From<serde_json::Error> for WebDriverError {
    fn from(err: serde_json::Error) -> WebDriverError {
        WebDriverError::new(ErrorStatus::InvalidArgument, err.to_string())
    }
}

impl From<io::Error> for WebDriverError {
    fn from(err: io::Error) -> WebDriverError {
        WebDriverError::new(ErrorStatus::UnknownError, err.to_string())
    }
}

impl From<DecodeError> for WebDriverError {
    fn from(err: DecodeError) -> WebDriverError {
        WebDriverError::new(ErrorStatus::UnknownError, err.to_string())
    }
}

impl From<Box<dyn error::Error>> for WebDriverError {
    fn from(err: Box<dyn error::Error>) -> WebDriverError {
        WebDriverError::new(ErrorStatus::UnknownError, err.to_string())
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;
    use crate::test::assert_ser;

    #[test]
    fn test_error_status_serialization() {
        assert_ser(&ErrorStatus::UnknownError, json!("unknown error"));
    }

    #[test]
    fn test_webdriver_error_json() {
        let data: Option<BTreeMap<Cow<'static, str>, Value>> = Some(
            [
                (Cow::Borrowed("foo"), Value::from(42)),
                (Cow::Borrowed("bar"), Value::from(vec![true])),
            ]
            .into_iter()
            .collect(),
        );

        let json = json!({"value": {
            "error": "unknown error",
            "message": "serialized error",
            "stacktrace": "foo\nbar",
            "data": {
                "foo": 42,
                "bar": [
                    true
                ]
            }
        }});

        let error = WebDriverError {
            error: ErrorStatus::UnknownError,
            message: "serialized error".into(),
            stack: "foo\nbar".into(),
            data,
            delete_session: true,
        };

        assert_ser(&error, json);
    }

    #[test]
    fn test_webdriver_error_new() {
        let json = json!({"value": {
            "error": "unknown error",
            "message": "error with default stack",
            "stacktrace": "",
        }});

        let error = WebDriverError::new::<Cow<'static, str>>(
            ErrorStatus::UnknownError,
            "error with default stack".into(),
        );
        assert_ser(&error, json);
    }

    #[test]
    fn test_webdriver_error_new_with_data_serialization() {
        let mut data_map = BTreeMap::new();
        data_map.insert("foo".into(), json!(42));
        data_map.insert("bar".into(), json!(vec!(true)));

        let error = WebDriverError::new_with_data(
            ErrorStatus::UnknownError,
            "serialization test",
            Some(data_map.clone()),
            Some("foo\nbar"),
        );

        let serialized = serde_json::to_string(&error).unwrap();
        let expected_json = json!({"value": {
            "error": "unknown error",
            "message": "serialization test",
            "stacktrace": "foo\nbar",
            "data": {
                "foo": 42,
                "bar": [true]
            }
        }});

        assert_eq!(
            serde_json::from_str::<Value>(&serialized).unwrap(),
            expected_json
        );
    }

    #[test]
    fn test_webdriver_error_new_with_data_no_data_no_stacktrace() {
        let error = WebDriverError::new_with_data(
            ErrorStatus::UnknownError,
            "error with no data and stack",
            None,
            None,
        );

        assert_eq!(error.error, ErrorStatus::UnknownError);
        assert_eq!(error.message, "error with no data and stack");
        assert_eq!(error.stack, "");
        assert_eq!(error.data, None);
    }

    #[test]
    fn test_webdriver_error_new_with_data_no_stacktrace() {
        let mut data_map = BTreeMap::new();
        data_map.insert("foo".into(), json!(42));

        let error = WebDriverError::new_with_data(
            ErrorStatus::UnknownError,
            "error with no stack",
            Some(data_map.clone()),
            None,
        );

        assert_eq!(error.error, ErrorStatus::UnknownError);
        assert_eq!(error.message, "error with no stack");
        assert_eq!(error.stack, "");
        assert_eq!(error.data, Some(data_map));
    }

    #[test]
    fn test_webdriver_error_new_with_data_no_data() {
        let error = WebDriverError::new_with_data(
            ErrorStatus::UnknownError,
            "error with no data",
            None,
            Some("foo\nbar"),
        );

        assert_eq!(error.error, ErrorStatus::UnknownError);
        assert_eq!(error.message, "error with no data");
        assert_eq!(error.stack, "foo\nbar");
        assert_eq!(error.data, None);
    }
}
