#![deny(unsafe_code)]
#![warn(clippy::todo)]

pub mod metrics;
/// Database abstraction layer for Kittybox, allowing the CMS to work with any kind of database.
pub mod database;
pub mod micropub;
pub mod indieauth;
pub mod frontend;

pub(crate) mod rejections {
    #[derive(Debug)]
    pub(crate) struct UnacceptableContentType;
    impl warp::reject::Reject for UnacceptableContentType {}

    #[derive(Debug)]
    pub(crate) struct HostHeaderUnset;
    impl warp::reject::Reject for HostHeaderUnset {}
}

pub static MICROPUB_CLIENT: &[u8] = include_bytes!("./index.html");

pub mod util {
    use warp::{Filter, host::Authority};
    use super::rejections;

    pub fn require_host() -> impl Filter<Extract = (Authority,), Error = warp::Rejection> + Copy {
        warp::host::optional()
            .and_then(|authority: Option<Authority>| async move {
                authority.ok_or_else(|| warp::reject::custom(rejections::HostHeaderUnset))
            })
    }

    pub fn template<R>(
        template: R
    ) -> impl warp::Reply
    where
        R: markup::Render + std::fmt::Display
    {
        warp::reply::html(template.to_string())
    }

    pub fn parse_accept() -> impl Filter<Extract = (http_types::Mime,), Error = warp::Rejection> + Copy {
        warp::header::value("Accept").and_then(|accept: warp::http::HeaderValue| async move {
            let mut accept: http_types::content::Accept = {
                // This is unneccesarily complicated because I want to reuse some http-types parsing
                // and http-types has constructor for Headers private so I need to construct
                // a mock Request to reason about headers... this is so dumb wtf
                // so much for zero-cost abstractions, huh
                let bytes: &[u8] = accept.as_bytes();
                let value = http_types::headers::HeaderValue::from_bytes(bytes.to_vec()).unwrap();
                let values: http_types::headers::HeaderValues = vec![value].into();
                let mut request = http_types::Request::new(http_types::Method::Get, "http://example.com/");
                request.append_header("Accept".parse::<http_types::headers::HeaderName>().unwrap(), &values);
                http_types::content::Accept::from_headers(&request).unwrap().unwrap()
            };

            // This code is INCREDIBLY dumb, honestly...
            // why did I even try to use it?
            // TODO vendor this stuff in so I can customize it
            match accept.negotiate(&[
                "text/html; encoding=\"utf-8\"".into(),
                "application/json; encoding=\"utf-8\"".into(),
                "text/html".into(),
                "application/json".into(),

            ]) {
                Ok(mime) => {
                    Ok(http_types::Mime::from(mime.value().as_str()))
                },
                Err(err) => {
                    log::error!("Content-Type negotiation error: {:?}, accepting: {:?}", err, accept);
                    Err(warp::reject::custom(rejections::UnacceptableContentType))
                }
            }
        })
    }

    mod tests {
        #[tokio::test]
        async fn test_require_host_with_host() {
            use super::require_host;

            let filter = require_host();

            let res = warp::test::request()
                .path("/")
                .header("Host", "localhost:8080")
                .filter(&filter)
                .await
                .unwrap();

            assert_eq!(res, "localhost:8080");
            
        }

        #[tokio::test]
        async fn test_require_host_no_host() {
            use super::require_host;

            let filter = require_host();

            let res = warp::test::request()
                .path("/")
                .filter(&filter)
                .await;

            assert!(res.is_err());
        }
    }
}
// fn equip_app<Storage>(mut app: App<Storage>) -> App<Storage>
// where
//     Storage: database::Storage + Send + Sync + Clone,
// {
//     app.at("/micropub")
//         .with(CORSMiddleware {})
//         .with(IndieAuthMiddleware::new())
//         .get(micropub::get_handler)
//         .post(micropub::post_handler);
//     // The Micropub client. It'll start small, but could grow into something full-featured
//     app.at("/micropub/client").get(|_: Request<_>| async move {
//         Ok(Response::builder(200)
//             .body(MICROPUB_CLIENT)
//             .content_type("text/html")
//             .build())
//     });
//     app.at("/")
//         .with(CORSMiddleware {})
//         .with(frontend::ErrorHandlerMiddleware {})
//         .get(frontend::mainpage)
//         .post(frontend::onboarding_receiver);
//     app.at("/login")
//         .with(frontend::ErrorHandlerMiddleware {})
//         .get(frontend::login::form)
//         .post(frontend::login::handler);
//     app.at("/login/callback")
//         .with(frontend::ErrorHandlerMiddleware {})
//         .get(frontend::login::callback);
//     app.at("/static/*path")
//         .with(frontend::ErrorHandlerMiddleware {})
//         .get(frontend::handle_static);
//     app.at("/*path")
//         .with(frontend::ErrorHandlerMiddleware {})
//         .get(frontend::render_post);
//     app.at("/coffee")
//         .with(frontend::ErrorHandlerMiddleware {})
//         .get(frontend::coffee);
//     // TODO make sure the health check actually checks the backend or something
//     // otherwise it'll get false-negatives for application faults like resource
//     // exhaustion
//     app.at("/health").get(|_| async { Ok("OK") });
//     app.at("/metrics").get(metrics::gather);

//     app.with(metrics::InstrumentationMiddleware {});
//     app.with(
//         tide::sessions::SessionMiddleware::new(
//             tide::sessions::CookieStore::new(),
//             app.state().cookie_secret.as_bytes(),
//         )
//         .with_cookie_name("kittybox_session")
//         .without_save_unchanged(),
//     );
//     app
// }

/*#[cfg(feature="redis")]
pub async fn get_app_with_redis(
    token_endpoint: surf::Url,
    authorization_endpoint: surf::Url,
    redis_uri: String,
    media_endpoint: Option<String>,
    internal_token: Option<String>,
) -> App<database::RedisStorage> {
    let app = tide::with_state(ApplicationState {
        token_endpoint,
        media_endpoint,
        authorization_endpoint,
        internal_token,
        storage: database::RedisStorage::new(redis_uri).await.unwrap(),
        http_client: surf::Client::new(),
    });

    equip_app(app)
}*/

/*#[cfg(test)]
pub async fn get_app_with_test_file(
    token_endpoint: surf::Url,
) -> (
    tempdir::TempDir,
    database::FileStorage,
    App<database::FileStorage>,
) {
    use surf::Url;
    let tempdir = tempdir::TempDir::new("file").expect("Failed to create tempdir");
    let backend = database::FileStorage::new(tempdir.path().to_path_buf())
        .await
        .unwrap();
    let app = tide::with_state(ApplicationState {
        token_endpoint,
        media_endpoint: None,
        authorization_endpoint: Url::parse("https://indieauth.com/auth").unwrap(),
        storage: backend.clone(),
        internal_token: None,
        cookie_secret: "1234567890abcdefghijklmnopqrstuvwxyz".to_string(),
        http_client: surf::Client::new(),
    });
    (tempdir, backend, equip_app(app))
}

#[cfg(all(redis, test))]
pub async fn get_app_with_test_redis(
    token_endpoint: surf::Url,
) -> (
    database::RedisInstance,
    database::RedisStorage,
    App<database::RedisStorage>,
) {
    use surf::Url;

    let redis_instance = database::get_redis_instance().await;
    let backend = database::RedisStorage::new(redis_instance.uri().to_string())
        .await
        .unwrap();
    let app = tide::with_state(ApplicationState {
        token_endpoint,
        media_endpoint: None,
        authorization_endpoint: Url::parse("https://indieauth.com/auth").unwrap(),
        storage: backend.clone(),
        internal_token: None,
        http_client: surf::Client::new(),
    });
    (redis_instance, backend, equip_app(app))
}*/

/*#[cfg(test)]
#[allow(unused_variables)]
mod tests {
    use super::*;
    use database::Storage;
    use mockito::mock;
    use serde_json::json;
    use tide_testing::TideTestingExt;

    // Helpers
    async fn create_app() -> (
        database::FileStorage,
        App<database::FileStorage>,
        tempdir::TempDir,
    ) {
        //get_app_with_memory_for_testing(surf::Url::parse(&*mockito::server_url()).unwrap()).await
        let (r, b, a) =
            get_app_with_test_file(surf::Url::parse(&*mockito::server_url()).unwrap()).await;
        (b, a, r)
    }

    async fn post_json(
        app: &App<database::FileStorage>,
        json: serde_json::Value,
    ) -> surf::Response {
        let request = app
            .post("/micropub")
            .header("Authorization", "Bearer test")
            .header("Content-Type", "application/json")
            .body(json);
        return request.send().await.unwrap();
    }

    #[async_std::test]
    async fn test_no_deletion_of_others_posts() {
        let _m = mock("GET", "/")
            .with_status(200)
            .with_header("Content-Type", "application/json")
            .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#)
            .create();

        let (db, app, _r) = create_app().await;

        let mut response = post_json(
            &app,
            json!({
                "type": ["h-entry"],
                "properties": {
                    "content": ["This is content!"]
                }
            }),
        )
        .await;
        println!(
            "{:#}",
            response.body_json::<serde_json::Value>().await.unwrap()
        );
        assert!(response.status() == 201 || response.status() == 202);
        let uid = response.header("Location").unwrap().last().to_string();
        drop(_m);
        let _m = mock("GET", "/")
            .with_status(200)
            .with_header("Content-Type", "application/json")
            .with_body(r#"{"me": "https://aaronparecki.com/", "client_id": "https://quill.p3k.io/", "scope": "create update delete media"}"#)
            .create();

        let mut response = app
            .post("/micropub")
            .header("Authorization", "Bearer awoo")
            .header("Content-Type", "application/json")
            .body(json!({ "action": "delete", "url": uid }))
            .send()
            .await
            .unwrap();
        println!("{}", response.body_string().await.unwrap());
        assert_eq!(response.status(), 403);
    }

    #[async_std::test]
    async fn test_no_posting_to_others_websites() {
        let _m = mock("GET", "/")
            .with_status(200)
            .with_header("Content-Type", "application/json")
            .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#)
            .create();

        let (db, app, _r) = create_app().await;

        let response = post_json(
            &app,
            json!({
                "type": ["h-entry"],
                "properties": {
                    "content": ["Fake news about Aaron Parecki!"],
                    "uid": ["https://aaronparecki.com/posts/fake-news"]
                }
            }),
        )
        .await;
        assert_eq!(response.status(), 403);

        let response = post_json(
            &app,
            json!({
                "type": ["h-entry"],
                "properties": {
                    "content": ["More fake news about Aaron Parecki!"],
                    "url": ["https://aaronparecki.com/posts/more-fake-news"]
                }
            }),
        )
        .await;
        // Should be posted successfully, but...
        assert!(response.status() == 201 || response.status() == 202);
        // ...won't be available on a foreign URL
        assert!(db
            .get_post("https://aaronparecki.com/posts/more-fake-news")
            .await
            .unwrap()
            .is_none());

        let response = post_json(&app, json!({
            "type": ["h-entry"],
            "properties": {
                "content": ["Sneaky advertisement designed to creep into someone else's feed! Buy whatever I'm promoting!"],
                "channel": ["https://aaronparecki.com/feeds/main"]
            }
        })).await;
        assert_eq!(response.status(), 403);
    }

    #[async_std::test]
    async fn test_successful_authorization() {
        let _m = mock("GET", "/")
            .with_status(200)
            .with_header("Content-Type", "application/json")
            .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#)
            .create();

        let (db, app, _r) = create_app().await;

        let response: serde_json::Value = app
            .get("/micropub?q=config")
            .header("Authorization", "test")
            .recv_json()
            .await
            .unwrap();
        assert!(!response["q"].as_array().unwrap().is_empty());
    }

    #[async_std::test]
    async fn test_unsuccessful_authorization() {
        let _m = mock("GET", "/")
            .with_status(400)
            .with_header("Content-Type", "application/json")
            .with_body(r#"{"error":"unauthorized","error_description":"A valid access token is required."}"#)
            .create();

        let (db, app, _r) = create_app().await;

        let response: surf::Response = app
            .get("/micropub?q=config")
            .header("Authorization", "test")
            .send()
            .await
            .unwrap();
        assert_eq!(response.status(), 401);
    }

    #[async_std::test]
    async fn test_no_auth_header() {
        let (db, app, _r) = create_app().await;

        let request: surf::RequestBuilder = app.get("/micropub?q=config");
        let response: surf::Response = request.send().await.unwrap();
        assert_eq!(response.status(), 401);
    }

    #[async_std::test]
    async fn test_create_post_form_encoded() {
        let _m = mock("GET", "/")
            .with_status(200)
            .with_header("Content-Type", "application/json")
            .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#)
            .create();

        let (storage, app, _r) = create_app().await;

        let request: surf::RequestBuilder = app
            .post("/micropub")
            .header("Authorization", "Bearer test")
            .header("Content-Type", "application/x-www-form-urlencoded")
            .body("h=entry&content=something%20interesting&category[]=test&category[]=stuff");
        let mut response: surf::Response = request.send().await.unwrap();
        println!(
            "{:#}",
            response.body_json::<serde_json::Value>().await.unwrap()
        );
        assert!(response.status() == 201 || response.status() == 202);
        let uid = response.header("Location").unwrap().last().to_string();
        // Assume the post is in the database at this point.
        let post = storage.get_post(&uid).await.unwrap().unwrap();
        assert_eq!(
            post["properties"]["content"][0]["html"]
                .as_str()
                .unwrap()
                .trim(),
            "<p>something interesting</p>"
        );
    }

    #[async_std::test]
    async fn test_create_post_json() {
        let _m = mock("GET", "/")
            .with_status(200)
            .with_header("Content-Type", "application/json")
            .with_body(r#"{"me": "https://fireburn.ru", "client_id": "https://quill.p3k.io/", "scope": "create update media"}"#)
            .create();

        let (storage, app, _r) = create_app().await;

        let mut response = post_json(
            &app,
            json!({
                "type": ["h-entry"],
                "properties": {
                    "content": ["This is content!"]
                }
            }),
        )
        .await;
        println!(
            "{:#}",
            response.body_json::<serde_json::Value>().await.unwrap()
        );
        assert!(response.status() == 201 || response.status() == 202);
        let uid = response.header("Location").unwrap().last().to_string();
        // Assume the post is in the database at this point.
        let post = storage.get_post(&uid).await.unwrap().unwrap();
        assert_eq!(
            post["properties"]["content"][0]["html"]
                .as_str()
                .unwrap()
                .trim(),
            "<p>This is content!</p>"
        );
        let feed = storage
            .get_post("https://fireburn.ru/feeds/main")
            .await
            .unwrap()
            .unwrap();
        assert_eq!(feed["children"].as_array().unwrap().len(), 1);
        assert_eq!(feed["children"][0].as_str().unwrap(), uid);
        let first_uid = uid;
        // Test creation of a second post
        let mut response = post_json(
            &app,
            json!({
                "type": ["h-entry"],
                "properties": {
                    "content": ["#moar content for you!"]
                }
            }),
        )
        .await;
        println!(
            "{:#}",
            response.body_json::<serde_json::Value>().await.unwrap()
        );
        assert!(response.status() == 201 || response.status() == 202);
        let uid = response.header("Location").unwrap().last().to_string();
        // Assume the post is in the database at this point.
        //println!("Keys in database: {:?}", storage.mapping.read().await.keys());
        let new_feed = storage
            .get_post("https://fireburn.ru/feeds/main")
            .await
            .unwrap()
            .unwrap();
        println!("{}", new_feed["children"]);
        assert_eq!(new_feed["children"].as_array().unwrap().len(), 2);
        assert_eq!(new_feed["children"][0].as_str().unwrap(), uid);
        assert_eq!(new_feed["children"][1].as_str().unwrap(), first_uid);
    }
}*/