#[cfg(debug_assertions)]
use log::info;
#[cfg(debug_assertions)]
use serde::Deserialize;
use tide::{Request, Response};

mod database;
mod indieauth;
mod micropub;

use crate::indieauth::check_auth;
use crate::micropub::{get_handler,post_handler};

#[derive(Clone)]
pub struct ApplicationState<StorageBackend>
where
    StorageBackend: database::Storage + Send + Sync + 'static
{
    token_endpoint: surf::Url,
    media_endpoint: Option<String>,
    http_client: surf::Client,
    storage: StorageBackend
}

type App<Storage> = tide::Server<ApplicationState<Storage>>;

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

#[cfg(debug_assertions)]
#[derive(Deserialize)]
struct Mf2JsonQuery {
    url: String,
    limit: usize,
    user: Option<String>,
    after: Option<String>
}

fn equip_app<Storage>(mut app: App<Storage>) -> App<Storage>
where
    Storage: database::Storage + Send + Sync + Clone
{
    app.at("/").get(|_: Request<_>| async move {
        Ok(Response::builder(200).body(INDEX_PAGE).content_type("text/html").build())
    });
    app.at("/micropub").with(check_auth).get(get_handler).post(post_handler);
    #[cfg(debug_assertions)]
    info!("Outfitting app with the debug function");
    #[cfg(debug_assertions)]
    app.at("/mf2-json").get(|req: Request<ApplicationState<Storage>>| async move {
        info!("DEBUG FUNCTION: Reading MF2-JSON");
        let backend = &req.state().storage;
        let query = req.query::<Mf2JsonQuery>()?;
        match backend.read_feed_with_limit(&query.url, &query.after, query.limit, &query.user).await {
            Ok(result) => match result {
                Some(post) => Ok(Response::builder(200).body(post).build()),
                None => Ok(Response::builder(404).build())
            },
            Err(err) => match err.kind() {
                database::ErrorKind::PermissionDenied => {
                    if let Some(_) = query.user {
                        Ok(Response::builder(403).build())
                    } else {
                        Ok(Response::builder(401).build())
                    }
                }
                _ => Ok(Response::builder(500).body(serde_json::json!({"error": "database_error", "error_description": format!("{}", err)})).build())
            }
        }
    });

    return app
}

pub async fn get_app_with_redis(token_endpoint: surf::Url, redis_uri: String, media_endpoint: Option<String>) -> App<database::RedisStorage> {
    let app = tide::with_state(ApplicationState { 
        token_endpoint, media_endpoint,
        storage: database::RedisStorage::new(redis_uri).await.unwrap(),
        http_client: surf::Client::new(),
    });
    
    equip_app(app)
}

#[cfg(test)]
pub async fn get_app_with_memory_for_testing(token_endpoint: surf::Url) -> (database::MemoryStorage, App<database::MemoryStorage>) {
    let database = database::MemoryStorage::new();
    let app = tide::with_state(ApplicationState {
        token_endpoint, media_endpoint: None,
        storage: database.clone(),
        http_client: surf::Client::new(),
    });

    return (database, equip_app(app))
}

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

    // Helpers
    async fn create_app() -> (database::MemoryStorage, App<database::MemoryStorage>) {
        get_app_with_memory_for_testing(surf::Url::parse(&*mockito::server_url()).unwrap()).await
    }

    async fn post_json(app: &App<database::MemoryStorage>, 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_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) = 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;
            assert_eq!(response.status(), 403);

            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) = 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) = 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) = 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) = 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) = 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);
    }
}