/* 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 std::error;
use std::fmt;

use serde::{Deserialize, Serialize};

#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum Error {
    Marionette(MarionetteError),
}

impl Error {
    pub fn kind(&self) -> ErrorKind {
        match *self {
            Error::Marionette(ref err) => err.kind,
        }
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::Marionette(ref err) => fmt
                .debug_struct("Marionette")
                .field("kind", &err.kind)
                .field("message", &err.message)
                .field("stacktrace", &err.stack.clone())
                .finish(),
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::Marionette(ref err) => write!(fmt, "{}: {}", err.kind, err.message),
        }
    }
}

impl error::Error for Error {
    fn description(&self) -> &str {
        match self {
            Error::Marionette(_) => self.kind().as_str(),
        }
    }
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct MarionetteError {
    #[serde(rename = "error")]
    pub kind: ErrorKind,
    #[serde(default = "empty_string")]
    pub message: String,
    #[serde(rename = "stacktrace", default = "empty_string")]
    pub stack: String,
}

fn empty_string() -> String {
    "".to_owned()
}

impl From<MarionetteError> for Error {
    fn from(error: MarionetteError) -> Error {
        Error::Marionette(error)
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum ErrorKind {
    #[serde(rename = "element click intercepted")]
    ElementClickIntercepted,
    #[serde(rename = "element not accessible")]
    ElementNotAccessible,
    #[serde(rename = "element not interactable")]
    ElementNotInteractable,
    #[serde(rename = "insecure certificate")]
    InsecureCertificate,
    #[serde(rename = "invalid argument")]
    InvalidArgument,
    #[serde(rename = "invalid cookie")]
    InvalidCookieDomain,
    #[serde(rename = "invalid element state")]
    InvalidElementState,
    #[serde(rename = "invalid selector")]
    InvalidSelector,
    #[serde(rename = "invalid session id")]
    InvalidSessionId,
    #[serde(rename = "javascript error")]
    JavaScript,
    #[serde(rename = "move target out of bounds")]
    MoveTargetOutOfBounds,
    #[serde(rename = "no such alert")]
    NoSuchAlert,
    #[serde(rename = "no such element")]
    NoSuchElement,
    #[serde(rename = "no such frame")]
    NoSuchFrame,
    #[serde(rename = "no such window")]
    NoSuchWindow,
    #[serde(rename = "script timeout")]
    ScriptTimeout,
    #[serde(rename = "session not created")]
    SessionNotCreated,
    #[serde(rename = "stale element reference")]
    StaleElementReference,
    #[serde(rename = "timeout")]
    Timeout,
    #[serde(rename = "unable to set cookie")]
    UnableToSetCookie,
    #[serde(rename = "unexpected alert open")]
    UnexpectedAlertOpen,
    #[serde(rename = "unknown command")]
    UnknownCommand,
    #[serde(rename = "unknown error")]
    Unknown,
    #[serde(rename = "unsupported operation")]
    UnsupportedOperation,
    #[serde(rename = "webdriver error")]
    WebDriver,
}

impl ErrorKind {
    pub(crate) fn as_str(self) -> &'static str {
        use ErrorKind::*;
        match self {
            ElementClickIntercepted => "element click intercepted",
            ElementNotAccessible => "element not accessible",
            ElementNotInteractable => "element not interactable",
            InsecureCertificate => "insecure certificate",
            InvalidArgument => "invalid argument",
            InvalidCookieDomain => "invalid cookie",
            InvalidElementState => "invalid element state",
            InvalidSelector => "invalid selector",
            InvalidSessionId => "invalid session id",
            JavaScript => "javascript error",
            MoveTargetOutOfBounds => "move target out of bounds",
            NoSuchAlert => "no such alert",
            NoSuchElement => "no such element",
            NoSuchFrame => "no such frame",
            NoSuchWindow => "no such window",
            ScriptTimeout => "script timeout",
            SessionNotCreated => "session not created",
            StaleElementReference => "stale eelement referencee",
            Timeout => "timeout",
            UnableToSetCookie => "unable to set cookie",
            UnexpectedAlertOpen => "unexpected alert open",
            UnknownCommand => "unknown command",
            Unknown => "unknown error",
            UnsupportedOperation => "unsupported operation",
            WebDriver => "webdriver error",
        }
    }
}

impl fmt::Display for ErrorKind {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        write!(fmt, "{}", self.as_str())
    }
}

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

    #[test]
    fn test_json_error() {
        let err = MarionetteError {
            kind: ErrorKind::Timeout,
            message: "".into(),
            stack: "".into(),
        };
        assert_ser_de(
            &err,
            json!({"error": "timeout", "message": "", "stacktrace": ""}),
        );
    }
}
