/* 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::{Map, Value};

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

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Locator {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub element: Option<String>,
    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(untagged)]
pub enum PrintPageRange {
    Integer(u64),
    Range(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<PrintPageRange>,
    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, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PrintOrientation {
    Landscape,
    #[default]
    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 SetPermissionParameters {
    pub descriptor: SetPermissionDescriptor,
    pub state: SetPermissionState,
}

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

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SetPermissionState {
    Denied,
    Granted,
    Prompt,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum AuthenticatorProtocol {
    #[serde(rename = "ctap1/u2f")]
    Ctap1U2f,
    #[serde(rename = "ctap2")]
    Ctap2,
    #[serde(rename = "ctap2_1")]
    Ctap2_1,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum AuthenticatorTransport {
    Usb,
    Nfc,
    Ble,
    SmartCard,
    Hybrid,
    Internal,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticatorParameters {
    pub protocol: AuthenticatorProtocol,
    pub transport: AuthenticatorTransport,
    pub has_resident_key: bool,
    pub has_user_verification: bool,
    pub is_user_consenting: bool,
    pub is_user_verified: bool,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticatorIdParameters {
    pub authenticator_id: String,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CredentialIdParameters {
    pub authenticator_id: String,
    pub credential_id: String,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CredentialParameters {
    pub authenticator_id: String,
    pub credentials: Credentials,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Credentials {
    pub credential_id: String,
    pub is_resident_credential: bool,
    pub rp_id: String,
    pub private_key: String,
    pub user_handle: Option<String>,
    pub sign_count: u64,
    pub large_blob: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UserVerificationParameters {
    pub authenticator_id: String,
    pub is_user_verified: bool,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct GlobalPrivacyControlParameters {
    pub gpc: bool,
}

#[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, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GeckoContext {
    Content,
    Chrome,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AddonInstallParameters {
    AddonInstallBase64 {
        addon: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        temporary: Option<bool>,
        #[serde(
            rename = "allowPrivateBrowsing",
            skip_serializing_if = "Option::is_none"
        )]
        allow_private_browsing: Option<bool>,
    },
    AddonInstallPath {
        path: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        temporary: Option<bool>,
        #[serde(
            rename = "allowPrivateBrowsing",
            skip_serializing_if = "Option::is_none"
        )]
        allow_private_browsing: Option<bool>,
    },
}

#[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: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: String },
    #[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:NewSession")]
    NewSession(Map<String, Value>),
    #[serde(rename = "WebDriver:NewWindow")]
    NewWindow(NewWindow),
    #[serde(rename = "WebDriver:PerformActions")]
    PerformActions(Map<String, Value>),
    #[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:SetPermission")]
    SetPermission(SetPermissionParameters),
    #[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")]
    TakeScreenshot(ScreenshotOptions),
    #[serde(rename = "GPC:GetGlobalPrivacyControl")]
    GPCGetGlobalPrivacyControl,
    #[serde(rename = "GPC:SetGlobalPrivacyControl")]
    GPCSetGlobalPrivacyControl(GlobalPrivacyControlParameters),
    #[serde(rename = "Addon:Install")]
    AddonInstall(AddonInstallParameters),
    #[serde(rename = "Addon:Uninstall")]
    AddonUninstall { id: String },
    #[serde(rename = "WebAuthn:AddCredential")]
    WebAuthnAddCredential(CredentialParameters),
    #[serde(rename = "WebAuthn:AddVirtualAuthenticator")]
    WebAuthnAddVirtualAuthenticator(AuthenticatorParameters),
    #[serde(rename = "WebAuthn:GetCredentials")]
    WebAuthnGetCredentials(AuthenticatorIdParameters),
    #[serde(rename = "WebAuthn:RemoveAllCredentials")]
    WebAuthnRemoveAllCredentials(AuthenticatorIdParameters),
    #[serde(rename = "WebAuthn:RemoveCredential")]
    WebAuthnRemoveCredential(CredentialIdParameters),
    #[serde(rename = "WebAuthn:RemoveVirtualAuthenticator")]
    WebAuthnRemoveVirtualAuthenticator(AuthenticatorIdParameters),
    #[serde(rename = "WebAuthn:SetUserVerified")]
    WebAuthnSetUserVerified(UserVerificationParameters),
    #[serde(rename = "Marionette:GetContext")]
    GetContext,
    #[serde(rename = "Marionette:SetContext")]
    SetContext { value: GeckoContext },
}

#[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 {
            element: None,
            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 {
            element: None,
            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::FindElement(Locator {
                element: Some("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"}}),
        );
    }
}
