mod templates;
pub use templates::{ErrorPage, MainPage, Template};
mod onboarding;
pub use onboarding::OnboardingPage;
mod indieauth;
pub use indieauth::AuthorizationRequestPage;
mod login;
pub use login::LoginPage;
mod mf2;
pub use mf2::{Entry, VCard, Feed, Food, POSTS_PER_PAGE};

pub mod admin;

pub mod assets {
    use axum::response::{IntoResponse, Response};
    use axum::extract::Path;
    use axum::http::StatusCode;
    use axum::http::header::{CONTENT_TYPE, CONTENT_ENCODING, CACHE_CONTROL};

    const ASSETS: include_dir::Dir<'static> = include_dir::include_dir!("$OUT_DIR/");
    const CACHE_FOR_A_DAY: &str = "max-age=86400";
    const GZIP: &str = "gzip";

    pub async fn statics(
        Path(path): Path<String>
    ) -> Response {
        let content_type: &'static str = if path.ends_with(".js") {
            "application/javascript"
        } else if path.ends_with(".css") {
            "text/css"
        } else if path.ends_with(".html") {
            "text/html; charset=\"utf-8\""
        } else {
            "application/octet-stream"
        };

        match ASSETS.get_file(path.clone() + ".gz") {
            Some(file) => (StatusCode::OK,
                           [(CONTENT_TYPE, content_type),
                            (CONTENT_ENCODING, GZIP),
                            (CACHE_CONTROL, CACHE_FOR_A_DAY)],
                           file.contents()).into_response(),
            None => match ASSETS.get_file(path) {
                Some(file) => (StatusCode::OK,
                               [(CONTENT_TYPE, content_type),
                                (CACHE_CONTROL, CACHE_FOR_A_DAY)],
                               file.contents()).into_response(),
                None => StatusCode::NOT_FOUND.into_response()
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use faker_rand::en_us::internet::Domain;
    use faker_rand::lorem::Word;
    use microformats::types::{Document, Item, PropertyValue, Url};
    use rand::distributions::Distribution;
    use serde_json::json;

    enum PostType {
        Note,
        Article,
        ReplyTo(serde_json::Value),
        ReplyToLink(String),
        LikeOf(serde_json::Value),
        LikeOfLink(String),
    }

    fn gen_hcard(domain: &str) -> serde_json::Value {
        use faker_rand::en_us::names::FirstName;

        json!({
            "type": ["h-card"],
            "properties": {
                "name": [rand::random::<FirstName>().to_string()],
                "photo": [format!("https://{domain}/media/me.png")],
                "uid": [format!("https://{domain}/")],
                "url": [format!("https://{domain}/")]
            }
        })
    }

    fn gen_random_post(domain: &str, kind: PostType) -> serde_json::Value {
        use faker_rand::lorem::{Paragraph, Sentence};

        fn html(content: Paragraph) -> serde_json::Value {
            json!({
                "html": format!("<p>{}</p>", content),
                "value": content.to_string()
            })
        }

        let uid = format!(
            "https://{domain}/posts/{}-{}-{}",
            rand::random::<Word>(),
            rand::random::<Word>(),
            rand::random::<Word>()
        );
        let dt = time::OffsetDateTime::now_utc()
            .to_offset(
                time::UtcOffset::from_hms(
                    rand::distributions::Uniform::new(-11, 12)
                        .sample(&mut rand::thread_rng()),
                    if rand::random::<bool>() { 0 } else { 30 },
                    0
                ).unwrap()
            )
            .format(&time::format_description::well_known::Rfc3339)
            .unwrap();

        match kind {
            PostType::Note => {
                let content = rand::random::<Paragraph>();

                json!({
                    "type": ["h-entry"],
                    "properties": {
                        "content": [html(content)],
                        "published": [dt],
                        "uid": [&uid], "url": [&uid],
                        "author": [gen_hcard(domain)]
                    }
                })
            }
            PostType::Article => {
                let content = rand::random::<Paragraph>();
                let name = rand::random::<Sentence>();

                json!({
                    "type": ["h-entry"],
                    "properties": {
                        "content": [html(content)],
                        "published": [dt],
                        "uid": [&uid], "url": [&uid],
                        "author": [gen_hcard(domain)],
                        "name": [name.to_string()]
                    }
                })
            }
            PostType::ReplyTo(ctx) => {
                let content = rand::random::<Paragraph>();

                json!({
                    "type": ["h-entry"],
                    "properties": {
                        "content": [html(content)],
                        "published": [dt],
                        "uid": [&uid], "url": [&uid],
                        "author": [gen_hcard(domain)],
                        "in-reply-to": [{
                            "type": ["h-cite"],
                            "properties": ctx["properties"]
                        }]
                    }
                })
            }
            PostType::ReplyToLink(link) => {
                let content = rand::random::<Paragraph>();

                json!({
                    "type": ["h-entry"],
                    "properties": {
                        "content": [html(content)],
                        "published": [dt],
                        "uid": [&uid], "url": [&uid],
                        "author": [gen_hcard(domain)],
                        "in-reply-to": [link]
                    }
                })
            }
            PostType::LikeOf(ctx) => {
                json!({
                    "type": ["h-entry"],
                    "properties": {
                        "published": [dt],
                        "author": [gen_hcard(domain)],
                        "uid": [&uid], "url": [&uid],
                        "like-of": [{
                            "type": ["h-cite"],
                            "properties": ctx["properties"]
                        }]
                    }
                })
            }
            PostType::LikeOfLink(link) => {
                json!({
                    "type": ["h-entry"],
                    "properties": {
                        "published": [dt],
                        "author": [gen_hcard(domain)],
                        "uid": [&uid], "url": [&uid],
                        "like-of": [link]
                    }
                })
            }
        }
    }

    fn check_dt_published(mf2: &serde_json::Value, item: &Item) {
        use microformats::types::temporal::Value as TemporalValue;

        assert!(item.properties.contains_key("published"));

        if let Some(PropertyValue::Temporal(TemporalValue::Timestamp(item))) =
            item.properties.get("published").and_then(|v| v.first())
        {
            // Faithfully reconstruct the original datetime
            // I wonder why not just have an Enum that would
            // get you either date, time or a datetime,
            // potentially with an offset?
            let offset = item.as_offset().unwrap().data;
            let date = item.as_date().unwrap().data;
            let time =  item.as_time().unwrap().data;

            let dt = date.with_time(time).assume_offset(offset);
            let expected = time::OffsetDateTime::parse(
                mf2["properties"]["published"][0].as_str().unwrap(),
                &time::format_description::well_known::Rfc3339
            ).unwrap();
            
            assert_eq!(dt, expected);
        } else {
            unreachable!()
        }
    }

    fn check_e_content(mf2: &serde_json::Value, item: &Item) {
        assert!(item.properties.contains_key("content"));

        if let Some(PropertyValue::Fragment(content)) = item.properties.get("content").and_then(|v| v.first())
        {
            assert_eq!(
                content.html,
                mf2["properties"]["content"][0]["html"].as_str().unwrap()
            );
        } else {
            unreachable!()
        }
    }

    #[test]
    fn test_note() {
        let mf2 = gen_random_post(&rand::random::<Domain>().to_string(), PostType::Note);

        let html = crate::mf2::Entry { post: &mf2 }.to_string();

        let url: Url = mf2
            .pointer("/properties/uid/0")
            .and_then(|i| i.as_str())
            .and_then(|u| u.parse().ok())
            .unwrap();
        let parsed: Document = microformats::from_html(&html, url.clone()).unwrap();

        if let Some(item) = parsed.into_iter().find(|i| i.properties.get("url").unwrap().contains(&PropertyValue::Url(url.clone()))) {
            let props = &item.properties;

            check_e_content(&mf2, &item);
            check_dt_published(&mf2, &item);
            assert!(props.contains_key("uid"));
            assert!(props.contains_key("url"));
            assert!(props
                .get("url")
                .unwrap()
                .iter()
                .any(|i| i == props.get("uid").and_then(|v| v.first()).unwrap()));
            // XXX: fails because of https://gitlab.com/maxburon/microformats-parser/-/issues/7
            assert!(!props.contains_key("name"));
        } else {
            unreachable!()
        }
    }

    #[test]
    fn test_article() {
        let mf2 = gen_random_post(&rand::random::<Domain>().to_string(), PostType::Article);
        let html = crate::mf2::Entry { post: &mf2 }.to_string();
        let url: Url = mf2
            .pointer("/properties/uid/0")
            .and_then(|i| i.as_str())
            .and_then(|u| u.parse().ok())
            .unwrap();
        let parsed: Document = microformats::from_html(&html, url.clone()).unwrap();

        if let Some(item) = parsed.into_iter().find(|i| i.properties.get("url").unwrap().contains(&PropertyValue::Url(url.clone()))) {

            check_e_content(&mf2, &item);
            check_dt_published(&mf2, &item);
            assert!(item.properties.contains_key("uid"));
            assert!(item.properties.contains_key("url"));
            assert!(item
                .properties
                .get("url")
                .unwrap()
                .iter()
                .any(|i| i == item.properties.get("uid").and_then(|v| v.first()).unwrap()));
            assert!(item.properties.contains_key("name"));
            if let Some(PropertyValue::Plain(name)) = item.properties.get("name").and_then(|v| v.first()) {
                assert_eq!(
                    name,
                    mf2.pointer("/properties/name/0")
                        .and_then(|v| v.as_str())
                        .unwrap()
                );
            } else {
                panic!("Name wasn't a plain property!");
            }
        } else {
            unreachable!()
        }
    }

    #[test]
    fn test_like_of() {
        for likeof in [
            PostType::LikeOf(gen_random_post(
                &rand::random::<Domain>().to_string(),
                PostType::Note,
            )),
            PostType::LikeOfLink(format!(
                "https://{}/posts/{}-{}-{}",
                &rand::random::<Domain>(),
                &rand::random::<Word>(),
                &rand::random::<Word>(),
                &rand::random::<Word>(),
            )),
        ] {
            let mf2 = gen_random_post(&rand::random::<Domain>().to_string(), likeof);
            let url: Url = mf2
                .pointer("/properties/uid/0")
                .and_then(|i| i.as_str())
                .and_then(|u| u.parse().ok())
                .unwrap();
            let html = crate::mf2::Entry { post: &mf2 }.to_string();
            let parsed: Document = microformats::from_html(&html, url.clone()).unwrap();

            if let Some(item) = parsed.items.first() {
                check_dt_published(&mf2, item);
                assert!(item.properties.contains_key("like-of"));
                match item.properties.get("like-of").and_then(|v| v.first()) {
                    Some(PropertyValue::Url(url)) => {
                        assert_eq!(
                            url,
                            &mf2.pointer("/properties/like-of/0")
                                .and_then(|i| i.as_str())
                                .or_else(|| mf2
                                    .pointer("/properties/like-of/0/properties/uid/0")
                                    .and_then(|i| i.as_str()))
                                .and_then(|u| u.parse::<Url>().ok())
                                .unwrap()
                        );
                    }
                    Some(PropertyValue::Item(_cite)) => {
                        todo!()
                    }
                    other => panic!("Unexpected value in like-of: {:?}", other),
                }
            } else {
                unreachable!()
            }
        }
    }
}