use crate::algorithms::ptd::{PostType, Type};
use crate::indieauth::AccessToken;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::string::ToString;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Visibility {
    Public,
    Unlisted,
    Private,
}

impl Default for Visibility {
    fn default() -> Visibility {
        Visibility::Public
    }
}

/// Represents the known high-level content containers in Microformats.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Kind {
    /// https://microformats.org/wiki/h-entry
    Entry,
    /// https://microformats.org/wiki/h-event
    Event,
    /// https://microformats.org/wiki/h-card
    Card,
    /// https://microformats.org/wiki/h-review
    Review,
    /// https://microformats.org/wiki/h-resume
    Resume,
    /// https://microformats.org/wiki/h-app
    /// https://indieweb.org/h-app
    Application,
}

impl ToString for Kind {
    /// Returns this kind as its known Microformat2 representation.
    fn to_string(&self) -> String {
        String::default()
    }
}

impl FromStr for Kind {
    type Err = std::convert::Infallible;
    /// Parses a string value that could a potential Microformats2 kind representation into its
    /// kind. Even if it doesn't recognize the type, it'll take the permissive approach by
    /// Microformats2 and convert it into an entry.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use Kind::*;
        Ok(match s {
            "event" => Event,
            "h-event" => Event,
            "h-x-app" => Application,
            "app" => Application,
            "h-app" => Application,
            "card" => Card,
            "h-card" => Card,
            "resume" => Resume,
            "h-resume" => Resume,
            _ => Entry,
        })
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Channel {
    pub uid: String,
    pub name: String,
}

/// Represents the normalized form of pagination in Micropub queries.
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Pagination {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub offset: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub filter: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub before: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub after: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub order: Option<String>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "q")]
pub enum Query {
    /// Pulls the configuration of the Micropub server.
    #[serde(rename = "config")]
    Configuration,

    /// Represents a call to `?q=source` for post information
    #[serde(rename_all = "kebab-case")]
    Source {
        url: Option<url::Url>,

        post_type: Option<Type>,

        #[serde(flatten)]
        pagination: Pagination,
    },

    /// Represents a call to `?q=channel` to fetch all channels.
    /// https://github.com/indieweb/micropub-extensions/issues/40
    #[serde(rename_all = "kebab-case")]
    Channel {
        #[serde(flatten)]
        pagination: Pagination,
    },

    #[serde(rename_all = "kebab-case")]
    SyndicateTo {
        post_type: Option<Type>,

        #[serde(flatten)]
        pagination: Pagination,
    },
}

/// Represents the response for a query.
// FIXME: Add the representation of syndication targets.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum QueryResponse {
    #[serde(rename_all = "kebab-case")]
    Paginated {
        items: Vec<serde_json::Value>,

        #[serde(default)]
        paging: Pagination,

        #[serde(alias = "_latest")]
        latest: Option<String>,

        #[serde(alias = "_earliest")]
        earliest: Option<String>,
    },

    #[serde(rename_all = "kebab-case")]
    Configuration {
        #[serde(default)]
        q: Vec<String>,

        #[serde(default)]
        channels: Vec<Channel>,

        #[serde(default)]
        media_endpoint: Option<url::Url>,

        #[serde(default)]
        post_types: Vec<PostType>,
    },
}

impl Query {
    /// Sends this request over to the requested Micropub server.
    pub async fn send<'a, C>(
        self: Box<Self>,
        config: Box<C>,
        endpoint: &url::Url,
        access_token: &AccessToken,
    ) -> Result<QueryResponse, crate::Error>
    where
        C: crate::IConfiguration + Send + Sync,
    {
        let full_url = endpoint;
        let resp = config
            .request(Some("Invoking a Micropub query".to_owned()))
            .request(reqwest::Method::GET, full_url.clone())
            .header("accept", "application/json")
            .bearer_auth(access_token.secret())
            .query(&self)
            .send()
            .await
            .map_err(|e| crate::Error::Other(anyhow::anyhow!(e)))?;

        let text = resp
            .text()
            .await
            .map_err(|e| crate::Error::Other(anyhow::anyhow!(e)))?;

        serde_json::from_str(&text).map_err(|e| crate::Error::Other(anyhow::anyhow!(e)))
    }
}

#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
    use super::*;
    use crate::test;
    use mockito::{self, Matcher};

    #[tokio::test(flavor = "multi_thread")]
    async fn Query_send_formats_query_result_correctly_for_a_single_url() {
        let cfg = test::init();
        let token = AccessToken::new("a-magic-token".to_owned());
        let upstream_url = url::Url::parse("http://wow.com").ok();
        let endpoint =
            url::Url::parse(format!("{}/micropub-endpoint", mockito::server_url()).as_str())
                .unwrap();
        let query = Query::Source {
            url: upstream_url.clone(),
            pagination: Pagination::default(),
            post_type: None,
        };

        let resp = QueryResponse::Paginated {
            items: vec![serde_json::json!({
                "type": "entry",
                "properties": {
                    "content": [
                        "this is some sample content"
                    ]
                }
            })],
            paging: Pagination::default(),
            earliest: None,
            latest: None,
        };

        let body = serde_json::to_value(&resp).unwrap().to_string();

        let endpoint_mock = mockito::mock("GET", "/micropub-endpoint")
            .with_body(&body)
            .match_header(
                "Authorization",
                format!("Bearer {}", token.secret()).as_str(),
            )
            .match_header("Accept", "application/json")
            .match_query(Matcher::AllOf(vec![
                Matcher::UrlEncoded("q".into(), "source".into()),
                Matcher::UrlEncoded("url".into(), upstream_url.unwrap().into()),
            ]))
            .expect(1)
            .create();

        let response = Query::send(Box::new(query), Box::new(cfg), &endpoint, &token)
            .await
            .unwrap();

        assert_eq!(response, resp);
        assert!(endpoint_mock.matched());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn Query_send_formats_query_result_correctly_for_post_type_list() {
        let cfg = test::init();
        let token = AccessToken::new("a-magic-token".to_owned());
        let endpoint =
            url::Url::parse(format!("{}/micropub-endpoint", mockito::server_url()).as_str())
                .unwrap();
        let query = Query::Source {
            url: None,
            pagination: Pagination::default(),
            post_type: Some(Type::Note),
        };
        let paging = Pagination::default();

        let resp = QueryResponse::Paginated {
            items: vec![serde_json::json!({
                "type": "entry",
                "properties": {
                    "content": [
                        "this is some sample content"
                    ]
                }
            })],
            paging,
            earliest: None,
            latest: None,
        };
        let query_str = serde_urlencoded::to_string(&query).unwrap();
        let body = serde_json::to_value(&resp).unwrap().to_string();

        let endpoint_mock = mockito::mock("GET", "/micropub-endpoint")
            .with_body(&body)
            .match_header(
                "Authorization",
                format!("Bearer {}", token.secret()).as_str(),
            )
            .match_header("Accept", "application/json")
            .match_query(Matcher::Exact(dbg!(query_str)))
            .expect(1)
            .create();

        let response = Query::send(Box::new(query), Box::new(cfg), &endpoint, &token)
            .await
            .unwrap();

        assert_eq!(response, resp);
        assert!(endpoint_mock.matched());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn Query_send_looks_up_configuration() {
        let cfg = test::init();
        let token = AccessToken::new("a-magic-token".to_owned());
        let endpoint =
            url::Url::parse(format!("{}/micropub-endpoint", mockito::server_url()).as_str())
                .unwrap();
        let query = Query::Configuration;

        let resp = QueryResponse::Configuration {
            media_endpoint: None,
            post_types: vec![],
            q: vec![],
            channels: vec![],
        };

        let body = serde_json::to_value(&resp).unwrap().to_string();

        let endpoint_mock = mockito::mock("GET", "/micropub-endpoint")
            .with_body(&body)
            .match_header(
                "Authorization",
                format!("Bearer {}", token.secret()).as_str(),
            )
            .match_header("Accept", "application/json")
            .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded(
                "q".into(),
                "config".into(),
            )]))
            .expect(1)
            .create();

        let response = Query::send(Box::new(query), Box::new(cfg), &endpoint, &token)
            .await
            .unwrap();

        assert_eq!(response, resp);
        assert!(endpoint_mock.matched());
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn Query_send_looks_up_channels() {
        let cfg = test::init();
        let token = AccessToken::new("a-magic-token".to_owned());
        let endpoint =
            url::Url::parse(format!("{}/micropub-endpoint", mockito::server_url()).as_str())
                .unwrap();
        let query = Query::Channel {
            pagination: Pagination::default(),
        };

        let resp = QueryResponse::Paginated {
            items: vec![serde_json::json!({
                "type": "entry",
                "properties": {
                    "content": [
                        "this is some sample content"
                    ]
                }
            })],
            paging: Pagination::default(),

            earliest: None,
            latest: None,
        };

        let body = serde_json::to_value(&resp).unwrap().to_string();

        let endpoint_mock = mockito::mock("GET", "/micropub-endpoint")
            .with_body(&body)
            .match_header(
                "Authorization",
                format!("Bearer {}", token.secret()).as_str(),
            )
            .match_header("Accept", "application/json")
            .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded(
                "q".into(),
                "channel".into(),
            )]))
            .expect(1)
            .create();

        let response = Query::send(Box::new(query), Box::new(cfg), &endpoint, &token)
            .await
            .unwrap();

        assert_eq!(response, resp);
        assert!(endpoint_mock.matched());
    }

    #[test]
    fn Pagination_from_string() {
        let parsed_pagination: Pagination = serde_qs::from_str("limit=20").unwrap();
        assert_eq!(parsed_pagination.limit, Some("20".to_string()));

        let warped_parsed_pagination: Pagination = serde_urlencoded::from_str("limit=20").unwrap();
        assert_eq!(warped_parsed_pagination.limit, Some("20".to_string()));
    }

    #[test]
    fn QueryResponse_from_string_for_pagination() {
        assert_eq!(
            serde_json::from_str::<QueryResponse>(
                r#"
                {
                    "items": [],
                    "paging": {}
                }
                "#
            )
            .unwrap(),
            QueryResponse::Paginated {
                items: vec![],
                paging: Pagination::default(),

                earliest: None,
                latest: None
            }
        );
    }

    #[test]
    fn QueryResponse_from_string_for_configuration() {
        assert_eq!(
            serde_json::from_str::<QueryResponse>(
                r#"
                {
                    "q": ["source", "post-types"],
                    "channels": ["pages", "posts"],
                    "media-endpoint": "http://micropub.endpoint",
                    "post-types": ["note", {"name": "Blog Posts", "type": "article"}]
                }
                "#
            )
            .unwrap(),
            QueryResponse::Configuration {
                q: vec!["source".to_string(), "post-types".to_string()],
                channels: vec!["pages".to_string(), "posts".to_string()],
                media_endpoint: "http://micropub.endpoint".parse().ok(),
                post_types: vec![
                    PostType::Simple(Type::Note),
                    PostType::Expanded {
                        name: "Blog Posts".to_string(),
                        kind: Type::Article,
                        h: "entry".to_string(),
                        properties: vec![],
                        required_properties: vec![]
                    }
                ]
            }
        );
    }
}
