/* 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::common::{Cookie, CredentialParameters};
use serde::ser::{Serialize, Serializer};
use serde_json::Value;

#[derive(Debug, PartialEq, Serialize)]
#[serde(untagged, remote = "Self")]
pub enum WebDriverResponse {
    NewWindow(NewWindowResponse),
    CloseWindow(CloseWindowResponse),
    Cookie(CookieResponse),
    Cookies(CookiesResponse),
    DeleteSession,
    ElementRect(ElementRectResponse),
    Generic(ValueResponse),
    WebAuthnAddVirtualAuthenticator(u64),
    WebAuthnGetCredentials(GetCredentialsResponse),
    NewSession(NewSessionResponse),
    Timeouts(TimeoutsResponse),
    Void,
    WindowRect(WindowRectResponse),
}

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

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

#[derive(Debug, PartialEq, Serialize)]
pub struct NewWindowResponse {
    pub handle: String,
    #[serde(rename = "type")]
    pub typ: String,
}

#[derive(Debug, PartialEq, Serialize)]
pub struct CloseWindowResponse(pub Vec<String>);

#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct CookieResponse(pub Cookie);

#[derive(Debug, PartialEq, Serialize)]
pub struct CookiesResponse(pub Vec<Cookie>);

#[derive(Debug, PartialEq, Serialize)]
pub struct ElementRectResponse {
    /// X axis position of the top-left corner of the element relative
    /// to the current browsing context’s document element in CSS reference
    /// pixels.
    pub x: f64,

    /// Y axis position of the top-left corner of the element relative
    /// to the current browsing context’s document element in CSS reference
    /// pixels.
    pub y: f64,

    /// Height of the element’s [bounding rectangle] in CSS reference
    /// pixels.
    ///
    /// [bounding rectangle]: https://drafts.fxtf.org/geometry/#rectangle
    pub width: f64,

    /// Width of the element’s [bounding rectangle] in CSS reference
    /// pixels.
    ///
    /// [bounding rectangle]: https://drafts.fxtf.org/geometry/#rectangle
    pub height: f64,
}

#[derive(Debug, PartialEq, Serialize)]
pub struct GetCredentialsResponse(pub Vec<CredentialParameters>);

#[derive(Debug, PartialEq, Serialize)]
pub struct NewSessionResponse {
    #[serde(rename = "sessionId")]
    pub session_id: String,
    pub capabilities: Value,
}

impl NewSessionResponse {
    pub fn new(session_id: String, capabilities: Value) -> NewSessionResponse {
        NewSessionResponse {
            session_id,
            capabilities,
        }
    }
}

#[derive(Debug, PartialEq, Serialize)]
pub struct TimeoutsResponse {
    pub script: Option<u64>,
    #[serde(rename = "pageLoad")]
    pub page_load: u64,
    pub implicit: u64,
}

impl TimeoutsResponse {
    pub fn new(script: Option<u64>, page_load: u64, implicit: u64) -> TimeoutsResponse {
        TimeoutsResponse {
            script,
            page_load,
            implicit,
        }
    }
}

#[derive(Debug, PartialEq, Serialize)]
pub struct ValueResponse(pub Value);

#[derive(Debug, PartialEq, Serialize)]
pub struct WindowRectResponse {
    /// `WindowProxy`’s [screenX] attribute.
    ///
    /// [screenX]: https://drafts.csswg.org/cssom-view/#dom-window-screenx
    pub x: i32,

    /// `WindowProxy`’s [screenY] attribute.
    ///
    /// [screenY]: https://drafts.csswg.org/cssom-view/#dom-window-screeny
    pub y: i32,

    /// Width of the top-level browsing context’s outer dimensions, including
    /// any browser chrome and externally drawn window decorations in CSS
    /// reference pixels.
    pub width: i32,

    /// Height of the top-level browsing context’s outer dimensions, including
    /// any browser chrome and externally drawn window decorations in CSS
    /// reference pixels.
    pub height: i32,
}

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

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

    #[test]
    fn test_json_new_window_response() {
        let json = json!({"value": {"handle": "42", "type": "window"}});
        let response = WebDriverResponse::NewWindow(NewWindowResponse {
            handle: "42".into(),
            typ: "window".into(),
        });

        assert_ser(&response, json);
    }

    #[test]
    fn test_json_close_window_response() {
        assert_ser(
            &WebDriverResponse::CloseWindow(CloseWindowResponse(vec!["1234".into()])),
            json!({"value": ["1234"]}),
        );
    }

    #[test]
    fn test_json_cookie_response_with_optional() {
        let json = json!({"value": {
            "name": "foo",
            "value": "bar",
            "path": "/",
            "domain": "foo.bar",
            "secure": true,
            "httpOnly": false,
            "expiry": 123,
            "sameSite": "Strict",
        }});
        let response = WebDriverResponse::Cookie(CookieResponse(Cookie {
            name: "foo".into(),
            value: "bar".into(),
            path: Some("/".into()),
            domain: Some("foo.bar".into()),
            expiry: Some(Date(123)),
            secure: true,
            http_only: false,
            same_site: Some("Strict".into()),
        }));

        assert_ser(&response, json);
    }

    #[test]
    fn test_json_cookie_response_without_optional() {
        let json = json!({"value": {
            "name": "foo",
            "value": "bar",
            "path": "/",
            "domain": null,
            "secure": true,
            "httpOnly": false,
        }});
        let response = WebDriverResponse::Cookie(CookieResponse(Cookie {
            name: "foo".into(),
            value: "bar".into(),
            path: Some("/".into()),
            domain: None,
            expiry: None,
            secure: true,
            http_only: false,
            same_site: None,
        }));

        assert_ser(&response, json);
    }

    #[test]
    fn test_json_cookies_response() {
        let json = json!({"value": [{
            "name": "name",
            "value": "value",
            "path": "/",
            "domain": null,
            "secure": true,
            "httpOnly": false,
            "sameSite": "None",
        }]});
        let response = WebDriverResponse::Cookies(CookiesResponse(vec![Cookie {
            name: "name".into(),
            value: "value".into(),
            path: Some("/".into()),
            domain: None,
            expiry: None,
            secure: true,
            http_only: false,
            same_site: Some("None".into()),
        }]));

        assert_ser(&response, json);
    }

    #[test]
    fn test_json_delete_session_response() {
        assert_ser(&WebDriverResponse::DeleteSession, json!({ "value": null }));
    }

    #[test]
    fn test_json_element_rect_response() {
        let json = json!({"value": {
            "x": 0.0,
            "y": 1.0,
            "width": 2.0,
            "height": 3.0,
        }});
        let response = WebDriverResponse::ElementRect(ElementRectResponse {
            x: 0f64,
            y: 1f64,
            width: 2f64,
            height: 3f64,
        });

        assert_ser(&response, json);
    }

    #[test]
    fn test_json_generic_value_response() {
        let response = {
            let mut value = Map::new();
            value.insert(
                "example".into(),
                Value::Array(vec![Value::String("test".into())]),
            );
            WebDriverResponse::Generic(ValueResponse(Value::Object(value)))
        };
        assert_ser(&response, json!({"value": {"example": ["test"]}}));
    }

    #[test]
    fn test_json_new_session_response() {
        let response =
            WebDriverResponse::NewSession(NewSessionResponse::new("id".into(), json!({})));
        assert_ser(
            &response,
            json!({"value": {"sessionId": "id", "capabilities": {}}}),
        );
    }

    #[test]
    fn test_json_timeouts_response() {
        assert_ser(
            &WebDriverResponse::Timeouts(TimeoutsResponse::new(Some(1), 2, 3)),
            json!({"value": {"script": 1, "pageLoad": 2, "implicit": 3}}),
        );
    }

    #[test]
    fn test_json_timeouts_response_with_null_script_timeout() {
        assert_ser(
            &WebDriverResponse::Timeouts(TimeoutsResponse::new(None, 2, 3)),
            json!({"value": {"script": null, "pageLoad": 2, "implicit": 3}}),
        );
    }

    #[test]
    fn test_json_void_response() {
        assert_ser(&WebDriverResponse::Void, json!({ "value": null }));
    }

    #[test]
    fn test_json_window_rect_response() {
        let json = json!({"value": {
            "x": 0,
            "y": 1,
            "width": 2,
            "height": 3,
        }});
        let response = WebDriverResponse::WindowRect(WindowRectResponse {
            x: 0i32,
            y: 1i32,
            width: 2i32,
            height: 3i32,
        });

        assert_ser(&response, json);
    }
}
