/* 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 serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::common::{from_cookie, from_name, to_cookie, to_name, Cookie, Frame, Timeouts, Window};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Url {
    pub url: String,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Locator {
    pub using: Selector,
    pub value: String,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Selector {
    #[serde(rename = "css selector")]
    Css,
    #[serde(rename = "link text")]
    LinkText,
    #[serde(rename = "partial link text")]
    PartialLinkText,
    #[serde(rename = "tag name")]
    TagName,
    #[serde(rename = "xpath")]
    XPath,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct NewWindow {
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub type_hint: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WindowRect {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub x: Option<i32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub y: Option<i32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub width: Option<i32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub height: Option<i32>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Keys {
    pub text: String,
    pub value: Vec<String>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PrintParameters {
    pub orientation: PrintOrientation,
    pub scale: f64,
    pub background: bool,
    pub page: PrintPage,
    pub margin: PrintMargins,
    pub page_ranges: Vec<String>,
    pub shrink_to_fit: bool,
}

impl Default for PrintParameters {
    fn default() -> Self {
        PrintParameters {
            orientation: PrintOrientation::default(),
            scale: 1.0,
            background: false,
            page: PrintPage::default(),
            margin: PrintMargins::default(),
            page_ranges: Vec::new(),
            shrink_to_fit: true,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PrintOrientation {
    Landscape,
    Portrait,
}

impl Default for PrintOrientation {
    fn default() -> Self {
        PrintOrientation::Portrait
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PrintPage {
    pub width: f64,
    pub height: f64,
}

impl Default for PrintPage {
    fn default() -> Self {
        PrintPage {
            width: 21.59,
            height: 27.94,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PrintMargins {
    pub top: f64,
    pub bottom: f64,
    pub left: f64,
    pub right: f64,
}

impl Default for PrintMargins {
    fn default() -> Self {
        PrintMargins {
            top: 1.0,
            bottom: 1.0,
            left: 1.0,
            right: 1.0,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ScreenshotOptions {
    pub id: Option<String>,
    pub highlights: Vec<Option<String>>,
    pub full: bool,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Script {
    pub script: String,
    pub args: Option<Vec<Value>>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Command {
    #[serde(rename = "WebDriver:AcceptAlert")]
    AcceptAlert,
    #[serde(
        rename = "WebDriver:AddCookie",
        serialize_with = "to_cookie",
        deserialize_with = "from_cookie"
    )]
    AddCookie(Cookie),
    #[serde(rename = "WebDriver:CloseWindow")]
    CloseWindow,
    #[serde(
        rename = "WebDriver:DeleteCookie",
        serialize_with = "to_name",
        deserialize_with = "from_name"
    )]
    DeleteCookie(String),
    #[serde(rename = "WebDriver:DeleteAllCookies")]
    DeleteCookies,
    #[serde(rename = "WebDriver:DeleteSession")]
    DeleteSession,
    #[serde(rename = "WebDriver:DismissAlert")]
    DismissAlert,
    #[serde(rename = "WebDriver:ElementClear")]
    ElementClear { id: String },
    #[serde(rename = "WebDriver:ElementClick")]
    ElementClick { id: String },
    #[serde(rename = "WebDriver:ElementSendKeys")]
    ElementSendKeys {
        id: String,
        text: String,
        value: Vec<String>,
    },
    #[serde(rename = "WebDriver:ExecuteAsyncScript")]
    ExecuteAsyncScript(Script),
    #[serde(rename = "WebDriver:ExecuteScript")]
    ExecuteScript(Script),
    #[serde(rename = "WebDriver:FindElement")]
    FindElement(Locator),
    #[serde(rename = "WebDriver:FindElements")]
    FindElements(Locator),
    #[serde(rename = "WebDriver:FindElement")]
    FindElementElement {
        element: String,
        using: Selector,
        value: String,
    },
    #[serde(rename = "WebDriver:FindElements")]
    FindElementElements {
        element: String,
        using: Selector,
        value: String,
    },
    #[serde(rename = "WebDriver:FindElementFromShadowRoot")]
    FindShadowRootElement {
        #[serde(rename = "shadowRoot")]
        shadow_root: String,
        using: Selector,
        value: String,
    },
    #[serde(rename = "WebDriver:FindElementsFromShadowRoot")]
    FindShadowRootElements {
        #[serde(rename = "shadowRoot")]
        shadow_root: String,
        using: Selector,
        value: String,
    },
    #[serde(rename = "WebDriver:FullscreenWindow")]
    FullscreenWindow,
    #[serde(rename = "WebDriver:Navigate")]
    Get(Url),
    #[serde(rename = "WebDriver:GetActiveElement")]
    GetActiveElement,
    #[serde(rename = "WebDriver:GetAlertText")]
    GetAlertText,
    #[serde(rename = "WebDriver:GetComputedLabel")]
    GetComputedLabel { id: String },
    #[serde(rename = "WebDriver:GetComputedRole")]
    GetComputedRole { id: String },
    #[serde(rename = "WebDriver:GetCookies")]
    GetCookies,
    #[serde(rename = "WebDriver:GetElementCSSValue")]
    GetCSSValue {
        id: String,
        #[serde(rename = "propertyName")]
        property: String,
    },
    #[serde(rename = "WebDriver:GetCurrentURL")]
    GetCurrentUrl,
    #[serde(rename = "WebDriver:GetElementAttribute")]
    GetElementAttribute { id: String, name: String },
    #[serde(rename = "WebDriver:GetElementProperty")]
    GetElementProperty { id: String, name: String },
    #[serde(rename = "WebDriver:GetElementRect")]
    GetElementRect { id: String },
    #[serde(rename = "WebDriver:GetElementTagName")]
    GetElementTagName { id: String },
    #[serde(rename = "WebDriver:GetElementText")]
    GetElementText { id: String },
    #[serde(rename = "WebDriver:GetPageSource")]
    GetPageSource,
    #[serde(rename = "WebDriver:GetShadowRoot")]
    GetShadowRoot { id: String },
    #[serde(rename = "WebDriver:GetTimeouts")]
    GetTimeouts,
    #[serde(rename = "WebDriver:GetTitle")]
    GetTitle,
    #[serde(rename = "WebDriver:GetWindowHandle")]
    GetWindowHandle,
    #[serde(rename = "WebDriver:GetWindowHandles")]
    GetWindowHandles,
    #[serde(rename = "WebDriver:GetWindowRect")]
    GetWindowRect,
    #[serde(rename = "WebDriver:Back")]
    GoBack,
    #[serde(rename = "WebDriver:Forward")]
    GoForward,
    #[serde(rename = "WebDriver:IsElementDisplayed")]
    IsDisplayed { id: String },
    #[serde(rename = "WebDriver:IsElementEnabled")]
    IsEnabled { id: String },
    #[serde(rename = "WebDriver:IsElementSelected")]
    IsSelected { id: String },
    #[serde(rename = "WebDriver:MaximizeWindow")]
    MaximizeWindow,
    #[serde(rename = "WebDriver:MinimizeWindow")]
    MinimizeWindow,
    #[serde(rename = "WebDriver:NewWindow")]
    NewWindow(NewWindow),
    #[serde(rename = "WebDriver:Print")]
    Print(PrintParameters),
    #[serde(rename = "WebDriver:Refresh")]
    Refresh,
    #[serde(rename = "WebDriver:ReleaseActions")]
    ReleaseActions,
    #[serde(rename = "WebDriver:SendAlertText")]
    SendAlertText(Keys),
    #[serde(rename = "WebDriver:SetTimeouts")]
    SetTimeouts(Timeouts),
    #[serde(rename = "WebDriver:SetWindowRect")]
    SetWindowRect(WindowRect),
    #[serde(rename = "WebDriver:SwitchToFrame")]
    SwitchToFrame(Frame),
    #[serde(rename = "WebDriver:SwitchToParentFrame")]
    SwitchToParentFrame,
    #[serde(rename = "WebDriver:SwitchToWindow")]
    SwitchToWindow(Window),
    #[serde(rename = "WebDriver:TakeScreenshot")]
    TakeElementScreenshot(ScreenshotOptions),
    #[serde(rename = "WebDriver:TakeScreenshot")]
    TakeFullScreenshot(ScreenshotOptions),
    #[serde(rename = "WebDriver:TakeScreenshot")]
    TakeScreenshot(ScreenshotOptions),
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::common::Date;
    use crate::test::{assert_ser, assert_ser_de};
    use serde_json::json;

    #[test]
    fn test_json_screenshot() {
        let data = ScreenshotOptions {
            id: None,
            highlights: vec![],
            full: false,
        };
        let json = json!({"full":false,"highlights":[],"id":null});
        assert_ser_de(&data, json);
    }

    #[test]
    fn test_json_selector_css() {
        assert_ser_de(&Selector::Css, json!("css selector"));
    }

    #[test]
    fn test_json_selector_link_text() {
        assert_ser_de(&Selector::LinkText, json!("link text"));
    }

    #[test]
    fn test_json_selector_partial_link_text() {
        assert_ser_de(&Selector::PartialLinkText, json!("partial link text"));
    }

    #[test]
    fn test_json_selector_tag_name() {
        assert_ser_de(&Selector::TagName, json!("tag name"));
    }

    #[test]
    fn test_json_selector_xpath() {
        assert_ser_de(&Selector::XPath, json!("xpath"));
    }

    #[test]
    fn test_json_selector_invalid() {
        assert!(serde_json::from_value::<Selector>(json!("foo")).is_err());
    }

    #[test]
    fn test_json_locator() {
        let json = json!({
            "using": "partial link text",
            "value": "link text",
        });
        let data = Locator {
            using: Selector::PartialLinkText,
            value: "link text".into(),
        };

        assert_ser_de(&data, json);
    }

    #[test]
    fn test_json_keys() {
        let data = Keys {
            text: "Foo".into(),
            value: vec!["F".into(), "o".into(), "o".into()],
        };
        let json = json!({"text": "Foo", "value": ["F", "o", "o"]});
        assert_ser_de(&data, json);
    }

    #[test]
    fn test_json_new_window() {
        let data = NewWindow {
            type_hint: Some("foo".into()),
        };
        assert_ser_de(&data, json!({ "type": "foo" }));
    }

    #[test]
    fn test_json_window_rect() {
        let data = WindowRect {
            x: Some(123),
            y: None,
            width: None,
            height: None,
        };
        assert_ser_de(&data, json!({"x": 123}));
    }

    #[test]
    fn test_command_with_params() {
        let locator = Locator {
            using: Selector::Css,
            value: "value".into(),
        };
        let json = json!({"WebDriver:FindElement": {"using": "css selector", "value": "value"}});
        assert_ser_de(&Command::FindElement(locator), json);
    }

    #[test]
    fn test_command_with_wrapper_params() {
        let cookie = Cookie {
            name: "hello".into(),
            value: "world".into(),
            path: None,
            domain: None,
            secure: false,
            http_only: false,
            expiry: Some(Date(1564488092)),
            same_site: None,
        };
        let json = json!({"WebDriver:AddCookie": {"cookie": {"name": "hello", "value": "world", "secure": false, "httpOnly": false, "expiry": 1564488092}}});
        assert_ser_de(&Command::AddCookie(cookie), json);
    }

    #[test]
    fn test_empty_commands() {
        assert_ser_de(&Command::GetTimeouts, json!("WebDriver:GetTimeouts"));
    }

    #[test]
    fn test_json_command_invalid() {
        assert!(serde_json::from_value::<Command>(json!("foo")).is_err());
    }

    #[test]
    fn test_json_delete_cookie_command() {
        let json = json!({"WebDriver:DeleteCookie": {"name": "foo"}});
        assert_ser_de(&Command::DeleteCookie("foo".into()), json);
    }

    #[test]
    fn test_json_new_window_command() {
        let data = NewWindow {
            type_hint: Some("foo".into()),
        };
        let json = json!({"WebDriver:NewWindow": {"type": "foo"}});
        assert_ser_de(&Command::NewWindow(data), json);
    }

    #[test]
    fn test_json_new_window_command_with_none_value() {
        let data = NewWindow { type_hint: None };
        let json = json!({"WebDriver:NewWindow": {}});
        assert_ser_de(&Command::NewWindow(data), json);
    }

    #[test]
    fn test_json_command_as_struct() {
        assert_ser(
            &Command::FindElementElement {
                element: "foo".into(),
                using: Selector::XPath,
                value: "bar".into(),
            },
            json!({"WebDriver:FindElement": {"element": "foo", "using": "xpath", "value": "bar" }}),
        );
    }

    #[test]
    fn test_json_get_computed_label_command() {
        assert_ser_de(
            &Command::GetComputedLabel { id: "foo".into() },
            json!({"WebDriver:GetComputedLabel": {"id": "foo"}}),
        );
    }

    #[test]
    fn test_json_get_computed_role_command() {
        assert_ser_de(
            &Command::GetComputedRole { id: "foo".into() },
            json!({"WebDriver:GetComputedRole": {"id": "foo"}}),
        );
    }

    #[test]
    fn test_json_get_css_value() {
        assert_ser_de(
            &Command::GetCSSValue {
                id: "foo".into(),
                property: "bar".into(),
            },
            json!({"WebDriver:GetElementCSSValue": {"id": "foo", "propertyName": "bar"}}),
        );
    }

    #[test]
    fn test_json_find_shadow_root_element() {
        assert_ser_de(
            &Command::FindShadowRootElement {
                shadow_root: "foo".into(),
                using: Selector::Css,
                value: "bar".into(),
            },
            json!({"WebDriver:FindElementFromShadowRoot": {"shadowRoot": "foo", "using": "css selector", "value": "bar"}}),
        );
    }

    #[test]
    fn test_json_find_shadow_root_elements() {
        assert_ser_de(
            &Command::FindShadowRootElements {
                shadow_root: "foo".into(),
                using: Selector::Css,
                value: "bar".into(),
            },
            json!({"WebDriver:FindElementsFromShadowRoot": {"shadowRoot": "foo", "using": "css selector", "value": "bar"}}),
        );
    }
}
