use crate::{middleware::AuthenticatedAdminMiddleware, model::Article, AppState};
use actix_web::{
    delete, get, post, put,
    web::{Data, Form, Json, Path},
    HttpRequest, HttpResponse, Responder,
};
use chrono::Utc;
use futures::stream::StreamExt;
use serde::{Deserialize, Serialize};
use wither::{
    bson::{doc, oid::ObjectId, DateTime},
    mongodb::Collection,
    prelude::Model,
};

#[derive(Deserialize, Serialize)]
pub struct ArticleTitleFormData {
    pub title: String,
}

fn get_collection(app_state: &AppState) -> Collection<Article> {
    app_state.db.collection_with_type::<Article>("articles")
}

#[post("/post-article")]
pub async fn post_article(
    app_state: Data<AppState>,
    article_data: Json<Article>,
    middleware: Data<AuthenticatedAdminMiddleware<'_>>,
    req: HttpRequest,
) -> impl Responder {
    if middleware.exec(&app_state, &req, None).await.is_err() {
        return HttpResponse::Unauthorized().finish();
    }

    let mut article_data = article_data.into_inner();
    article_data.date = Some(DateTime(Utc::now()));

    match get_collection(&app_state)
        .insert_one(article_data, None)
        .await
    {
        Ok(res) => HttpResponse::Created().json(res),
        Err(e) => {
            HttpResponse::InternalServerError().body(format!("Error inserting new article {:?}", e))
        }
    }
}

#[put("/update-article/{article_id}")]
pub async fn update_article(
    app_state: Data<AppState>,
    article_data: Json<Article>,
    middleware: Data<AuthenticatedAdminMiddleware<'_>>,
    article_id: Path<String>,
    req: HttpRequest,
) -> impl Responder {
    if middleware.exec(&app_state, &req, None).await.is_err() {
        return HttpResponse::Unauthorized().finish();
    }

    let article_id = match ObjectId::with_string(&article_id.into_inner()) {
        Ok(id) => id,
        Err(_) => {
            return HttpResponse::BadRequest()
                .body("Failed to convert article_id to ObjectId. String may be malformed")
        }
    };

    let mut article_data = article_data.into_inner();
    article_data.date = Some(DateTime(Utc::now()));

    match get_collection(&app_state)
        .find_one_and_replace(doc! {"_id": &article_id}, article_data, None)
        .await
    {
        Ok(res) => HttpResponse::Ok().json(res.unwrap()),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

#[delete("/delete-article/{article_id}")]
pub async fn delete_article(
    app_state: Data<AppState>,
    middleware: Data<AuthenticatedAdminMiddleware<'_>>,
    article_id: Path<String>,
    req: HttpRequest,
) -> impl Responder {
    if middleware.exec(&app_state, &req, None).await.is_err() {
        return HttpResponse::Unauthorized().finish();
    }

    let article_id = match ObjectId::with_string(&article_id.into_inner()) {
        Ok(id) => id,
        Err(_) => {
            return HttpResponse::BadRequest()
                .body("Failed to convert article_id to ObjectId. String may be malformed")
        }
    };

    match get_collection(&app_state)
        .find_one_and_delete(doc! {"_id": &article_id}, None)
        .await
    {
        Ok(_) => HttpResponse::Accepted().body("Article was deleted"),
        Err(e) => HttpResponse::InternalServerError().body(&format!("{:?}", e)),
    }
}

#[get("/articles/{category}")]
pub async fn get_articles_by_category(
    app_state: Data<AppState>,
    category: Path<String>,
) -> impl Responder {
    match get_collection(&app_state)
        .find(doc! {"category": category.into_inner()}, None)
        .await
    {
        Ok(mut cursor) => {
            let mut results: Vec<Article> = Vec::new();

            while let Some(result) = cursor.next().await {
                match result {
                    Ok(article) => {
                        results.push(article);
                    }
                    Err(_) => {
                        return HttpResponse::InternalServerError().finish();
                    }
                }
            }
            HttpResponse::Ok().json(results)
        }
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

#[get("/article/{article_id}")]
pub async fn get_article(app_state: Data<AppState>, article_id: Path<String>) -> impl Responder {
    let article_id = match ObjectId::with_string(&article_id.into_inner()) {
        Ok(id) => id,
        Err(_) => {
            return HttpResponse::BadRequest()
                .body("Failed to convert article_id to ObjectId. String may be malformed")
        }
    };

    match Article::find_one(&app_state.db, doc! {"_id":&article_id}, None).await {
        Ok(art) => {
            if art.is_none() {
                return HttpResponse::NotFound().body("Article was not found");
            }

            HttpResponse::Ok().json(art)
        }
        Err(e) => HttpResponse::InternalServerError().body(format!("Database error: {:#?}", e)),
    }
}

#[post("/article-by-title")]
pub async fn get_article_by_title(
    app_state: Data<AppState>,
    form_data: Form<ArticleTitleFormData>,
) -> impl Responder {
    let title = form_data.into_inner().title;
    match Article::find_one(&app_state.db, doc! {"title":title}, None).await {
        Ok(art) => {
            if art.is_none() {
                return HttpResponse::NotFound().body("Article was not found");
            }

            HttpResponse::Ok().json(art)
        }
        Err(e) => HttpResponse::InternalServerError().body(format!("Database error: {:#?}", e)),
    }
}

#[get("/articles")]
pub async fn get_all_articles(app_state: Data<AppState>) -> impl Responder {
    match get_collection(&app_state).find(None, None).await {
        Ok(mut cursor) => {
            let mut results: Vec<Article> = Vec::new();
            while let Some(result) = cursor.next().await {
                match result {
                    Ok(article) => {
                        results.push(article);
                    }
                    Err(_) => {
                        return HttpResponse::InternalServerError().finish();
                    }
                }
            }
            HttpResponse::Ok().json(results)
        }
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
 *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
 *  _______   ______    ______   _______   *@@
 * |__   __@ |  ____@  /  ____@ |__   __@  *@@
 *    |  @   |  @__    \_ @_       |  @    *@@
 *    |  @   |   __@     \  @_     |  @    *@@
 *    |  @   |  @___   ____\  @    |  @    *@@
 *    |__@   |______@  \______@    |__@    *@@
 *                                         *@@
 *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
 *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/

#[cfg(test)]
mod test_articles {
    use super::*;
    use crate::middleware::get_auth_cookie;
    use crate::model::{AdminAuthCredentials, Administrator};
    use actix_web::{
        http::{Method, StatusCode},
        test,
        web::Bytes,
        App,
    };
    use wither::bson::Bson;

    async fn insert_test_article(
        app_state: &AppState,
        test_article: Article,
    ) -> Result<(ObjectId, String), String> {
        let title = test_article.title.to_owned();
        match get_collection(&app_state)
            .insert_one(test_article, None)
            .await
        {
            Ok(inserted) => match inserted.inserted_id {
                Bson::ObjectId(id) => Ok((id, title)),
                _ => Err(String::from("Failed to parse inserted_id")),
            },
            Err(e) => Err(format!("{:?}", e)),
        }
    }

    async fn delete_test_article(
        app_state: &AppState,
        article_id: &ObjectId,
    ) -> Result<i64, String> {
        match get_collection(&app_state)
            .delete_one(doc! {"_id": article_id}, None)
            .await
        {
            Ok(delete_result) => Ok(delete_result.deleted_count),
            Err(e) => Err(format!("{:?}", e)),
        }
    }

    async fn get_authenticated_admin(app_state: &AppState) -> Administrator {
        Administrator::authenticated(
            app_state,
            AdminAuthCredentials {
                username: app_state.env.default_admin_username.to_owned(),
                password: app_state.env.default_admin_password.to_owned(),
            },
        )
        .await
        .unwrap()
    }

    #[tokio::test]
    async fn test_post_article() {
        dotenv::dotenv().ok();

        let app_state = AppState::for_test().await;

        let mut app = test::init_service(
            App::new()
                .app_data(Data::new(app_state.clone()))
                .app_data(Data::new(AuthenticatedAdminMiddleware::new(
                    "kuadrado-admin-auth",
                )))
                .service(post_article),
        )
        .await;

        let article = Article::test_article();

        let admin_user = get_authenticated_admin(&app_state).await;

        let req = test::TestRequest::with_uri("/post-article")
            .method(Method::POST)
            .header("Content-Type", "application/json")
            .header("Accept", "text/html")
            .cookie(get_auth_cookie(
                "kuadrado-admin-auth",
                app_state
                    .encryption
                    .decrypt(&admin_user.auth_token.unwrap())
                    .to_owned(),
            ))
            .set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
            .to_request();

        let resp = test::call_service(&mut app, req).await;

        assert_eq!(resp.status(), StatusCode::CREATED);

        let find_inserted = Article::find_one(&app_state.db, doc! {"title": &article.title}, None)
            .await
            .unwrap();

        assert!(find_inserted.is_some());
        assert_eq!(find_inserted.unwrap().title, article.title);

        get_collection(&app_state)
            .delete_one(doc! {"title": article.title}, None)
            .await
            .unwrap();
    }

    #[tokio::test]
    async fn test_post_article_unauthorized() {
        dotenv::dotenv().ok();

        let app_state = AppState::for_test().await;

        let mut app = test::init_service(
            App::new()
                .app_data(Data::new(app_state.clone()))
                .app_data(Data::new(AuthenticatedAdminMiddleware::new(
                    "kuadrado-admin-auth",
                )))
                .service(post_article),
        )
        .await;

        let article = Article::test_article();

        let req = test::TestRequest::with_uri("/post-article")
            .method(Method::POST)
            .header("Content-Type", "application/json")
            .header("Accept", "text/html")
            .cookie(get_auth_cookie(
                "wrong-cookie",
                app_state.encryption.random_ascii_lc_string(32),
            ))
            .set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
            .to_request();

        let resp = test::call_service(&mut app, req).await;

        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
    }

    #[tokio::test]
    async fn test_update_article() {
        dotenv::dotenv().ok();

        let app_state = AppState::for_test().await;

        let mut app = test::init_service(
            App::new()
                .app_data(Data::new(app_state.clone()))
                .app_data(Data::new(AuthenticatedAdminMiddleware::new(
                    "kuadrado-admin-auth",
                )))
                .service(update_article),
        )
        .await;

        let mut article = Article::test_article();

        let (article_id, _) = insert_test_article(&app_state, article.clone())
            .await
            .unwrap();

        article.title = "changed title".to_string();

        let admin_user = get_authenticated_admin(&app_state).await;

        let req = test::TestRequest::with_uri(
            format!("/update-article/{}", article_id.to_hex()).as_str(),
        )
        .method(Method::PUT)
        .header("Content-Type", "application/json")
        .header("Accept", "text/html")
        .cookie(get_auth_cookie(
            "kuadrado-admin-auth",
            app_state
                .encryption
                .decrypt(&admin_user.auth_token.unwrap())
                .to_owned(),
        ))
        .set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
        .to_request();

        let resp = test::call_service(&mut app, req).await;

        assert_eq!(resp.status(), StatusCode::OK);

        let find_inserted = Article::find_one(&app_state.db, doc! {"_id": &article_id}, None)
            .await
            .unwrap();

        assert!(find_inserted.is_some());
        assert_eq!(find_inserted.unwrap().title, "changed title");

        let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
        assert_eq!(del_count, 1);
    }

    #[tokio::test]
    async fn test_update_article_unauthorized() {
        dotenv::dotenv().ok();

        let app_state = AppState::for_test().await;

        let mut app = test::init_service(
            App::new()
                .app_data(Data::new(app_state.clone()))
                .app_data(Data::new(AuthenticatedAdminMiddleware::new(
                    "kuadrado-admin-auth",
                )))
                .service(update_article),
        )
        .await;

        let article = Article::test_article();

        let req = test::TestRequest::with_uri(
            format!("/update-article/{}", ObjectId::new().to_hex()).as_str(),
        )
        .method(Method::PUT)
        .header("Content-Type", "application/json")
        .header("Accept", "text/html")
        .cookie(get_auth_cookie(
            "wrong-cookie",
            app_state.encryption.random_ascii_lc_string(32),
        ))
        .set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
        .to_request();

        let resp = test::call_service(&mut app, req).await;

        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
    }

    #[tokio::test]
    async fn test_delete_article() {
        dotenv::dotenv().ok();

        let app_state = AppState::for_test().await;

        let mut app = test::init_service(
            App::new()
                .app_data(Data::new(app_state.clone()))
                .app_data(Data::new(AuthenticatedAdminMiddleware::new(
                    "kuadrado-admin-auth",
                )))
                .service(delete_article),
        )
        .await;

        let article = Article::test_article();
        let (article_id, _) = insert_test_article(&app_state, article.clone())
            .await
            .unwrap();

        let admin_user = get_authenticated_admin(&app_state).await;

        let req = test::TestRequest::with_uri(
            format!("/delete-article/{}", article_id.to_hex()).as_str(),
        )
        .method(Method::DELETE)
        .cookie(get_auth_cookie(
            "kuadrado-admin-auth",
            app_state
                .encryption
                .decrypt(&admin_user.auth_token.unwrap())
                .to_owned(),
        ))
        .to_request();

        let resp = test::call_service(&mut app, req).await;

        assert_eq!(resp.status(), StatusCode::ACCEPTED);

        let find_inserted = Article::find_one(&app_state.db, doc! {"_id": &article_id}, None)
            .await
            .unwrap();

        assert!(find_inserted.is_none());
    }

    #[tokio::test]
    async fn test_delete_article_unauthorized() {
        dotenv::dotenv().ok();

        let app_state = AppState::for_test().await;

        let mut app = test::init_service(
            App::new()
                .app_data(Data::new(app_state.clone()))
                .app_data(Data::new(AuthenticatedAdminMiddleware::new(
                    "kuadrado-admin-auth",
                )))
                .service(delete_article),
        )
        .await;

        let article = Article::test_article();

        let req = test::TestRequest::with_uri(
            format!("/delete-article/{}", ObjectId::new().to_hex()).as_str(),
        )
        .method(Method::DELETE)
        .cookie(get_auth_cookie(
            "wrong-cookie",
            app_state.encryption.random_ascii_lc_string(32),
        ))
        .set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
        .to_request();

        let resp = test::call_service(&mut app, req).await;

        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
    }

    #[tokio::test]
    async fn test_get_article() {
        dotenv::dotenv().ok();

        let app_state = AppState::for_test().await;

        let mut app = test::init_service(
            App::new()
                .app_data(Data::new(app_state.clone()))
                .service(get_article),
        )
        .await;

        let article = Article::test_article();
        let (article_id, article_title) = insert_test_article(&app_state, article.clone())
            .await
            .unwrap();

        let req = test::TestRequest::with_uri(format!("/article/{}", article_id.to_hex()).as_str())
            .header("Accept", "application/json")
            .method(Method::GET)
            .to_request();

        let resp = test::call_service(&mut app, req).await;

        assert_eq!(resp.status(), StatusCode::OK);

        let result: Article = test::read_body_json(resp).await;
        assert_eq!(result.title, article_title);

        let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
        assert_eq!(del_count, 1);
    }

    #[tokio::test]
    async fn test_get_article_by_title() {
        dotenv::dotenv().ok();

        let app_state = AppState::for_test().await;

        let mut app = test::init_service(
            App::new()
                .app_data(Data::new(app_state.clone()))
                .service(get_article_by_title),
        )
        .await;

        let article = Article::test_article();
        let (article_id, article_title) = insert_test_article(&app_state, article.clone())
            .await
            .unwrap();

        let req = test::TestRequest::with_uri("/article-by-title")
            .header("Accept", "application/json")
            .method(Method::POST)
            .set_form(&ArticleTitleFormData {
                title: article_title.to_owned(),
            })
            .to_request();

        let resp = test::call_service(&mut app, req).await;

        assert_eq!(resp.status(), StatusCode::OK);
        let result: Article = test::read_body_json(resp).await;
        assert_eq!(result.title, article_title);

        let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
        assert_eq!(del_count, 1);
    }

    #[tokio::test]
    async fn test_get_articles_by_category() {
        dotenv::dotenv().ok();

        let app_state = AppState::for_test().await;

        let mut app = test::init_service(
            App::new()
                .app_data(Data::new(app_state.clone()))
                .service(get_articles_by_category),
        )
        .await;

        let article = Article::test_article();
        let (article_id, article_title) = insert_test_article(&app_state, article.clone())
            .await
            .unwrap();

        let req = test::TestRequest::with_uri("/articles/testing")
            .header("Accept", "application/json")
            .method(Method::GET)
            .to_request();

        let resp = test::call_service(&mut app, req).await;

        assert_eq!(resp.status(), StatusCode::OK);

        let results: Vec<Article> = test::read_body_json(resp).await;
        let find_inserted = results.iter().find(|&art| art.title == article_title);
        assert!(find_inserted.is_some());

        let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
        assert_eq!(del_count, 1);
    }
}