use axum::{
    extract::Path,
    http::{header::{AUTHORIZATION, CONTENT_TYPE}, HeaderMap, Method},
    routing::{get, post, put},
    Json, Router,
};
use tower_http::cors::{Any, CorsLayer};

use crate::{
    core::Core,
    database::UserId,
    handlers::{
        check_password, create_game, create_user, get_user, healthcheck, set_password,
        wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest,
    },
};

pub fn routes(core: Core) -> Router {
    Router::new()
        .route(
            "/api/v1/health",
            get({
                let core = core.clone();
                move || healthcheck(core)
            })
            .layer(
                CorsLayer::new()
                    .allow_methods([Method::GET])
                    .allow_origin(Any),
            ),
        )
        .route(
            "/api/v1/auth",
            post({
                let core = core.clone();
                move |req: Json<AuthRequest>| wrap_handler(|| check_password(core, req))
            })
            .layer(
                CorsLayer::new()
                    .allow_methods([Method::POST])
                    .allow_headers([CONTENT_TYPE])
                    .allow_origin(Any),
            ),
        )
        .route(
            // By default, just get the self user.
            "/api/v1/user",
            get({
                let core = core.clone();
                move |headers: HeaderMap| wrap_handler(|| get_user(core, headers, None))
            })
            .layer(
                CorsLayer::new()
                    .allow_methods([Method::GET])
                    .allow_headers([AUTHORIZATION])
                    .allow_origin(Any),
            )
            .put({
                let core = core.clone();
                move |headers: HeaderMap, req: Json<CreateUserRequest>| {
                    let Json(req) = req;
                    wrap_handler(|| create_user(core, headers, req))
                }
            }),
        )
        .route(
            "/api/v1/user/password",
            put({
                let core = core.clone();
                move |headers: HeaderMap, req: Json<SetPasswordRequest>| {
                    let Json(req) = req;
                    wrap_handler(|| set_password(core, headers, req))
                }
            }),
        )
        .route(
            "/api/v1/user/:user_id",
            get({
                let core = core.clone();
                move |user_id: Path<UserId>, headers: HeaderMap| {
                    let Path(user_id) = user_id;
                    wrap_handler(|| get_user(core, headers, Some(user_id)))
                }
            }),
        )
        .route(
            "/api/v1/games",
            put({
                let core = core.clone();
                move |headers: HeaderMap, req: Json<CreateGameRequest>| {
                    let Json(req) = req;
                    wrap_handler(|| create_game(core, headers, req))
                }
            }),
        )
}

#[cfg(test)]
mod test {
    use std::path::PathBuf;

    use axum::http::StatusCode;
    use axum_test::TestServer;
    use cool_asserts::assert_matches;
    use result_extended::ResultExt;

    use super::*;
    use crate::{
        asset_db::FsAssets,
        core::Core,
        database::{Database, DbConn, GameId, SessionId, UserId},
        handlers::CreateGameRequest,
        types::UserProfile,
    };

    fn setup_without_admin() -> (Core, TestServer) {
        let memory_db: Option<PathBuf> = None;
        let conn = DbConn::new(memory_db);
        let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
        let app = routes(core.clone());
        let server = TestServer::new(app).unwrap();
        (core, server)
    }

    async fn setup_admin_enabled() -> (Core, TestServer) {
        let memory_db: Option<PathBuf> = None;
        let conn = DbConn::new(memory_db);
        conn.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
            .await
            .unwrap();
        let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
        let app = routes(core.clone());
        let server = TestServer::new(app).unwrap();
        (core, server)
    }

    async fn setup_with_user() -> (Core, TestServer) {
        let (core, server) = setup_admin_enabled().await;
        let response = server
            .post("/api/v1/auth")
            .json(&AuthRequest {
                username: "admin".to_owned(),
                password: "aoeu".to_owned(),
            })
            .await;
        response.assert_status_ok();
        let session_id: Option<SessionId> = response.json();
        let session_id = session_id.unwrap();

        let response = server
            .put("/api/v1/user")
            .add_header("Authorization", format!("Bearer {}", session_id))
            .json(&CreateUserRequest {
                username: "savanni".to_owned(),
            })
            .await;
        response.assert_status_ok();

        let response = server
            .put("/api/v1/user")
            .add_header("Authorization", format!("Bearer {}", session_id))
            .json(&CreateUserRequest {
                username: "shephard".to_owned(),
            })
            .await;
        response.assert_status_ok();

        (core, server)
    }

    #[tokio::test]
    async fn it_returns_a_healthcheck() {
        let (core, server) = setup_without_admin();

        let response = server.get("/api/v1/health").await;
        response.assert_status_ok();
        let b: crate::handlers::HealthCheck = response.json();
        assert_eq!(b, crate::handlers::HealthCheck { ok: false });

        assert_matches!(
            core.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
                .await,
            ResultExt::Ok(_)
        );

        let response = server.get("/api/v1/health").await;
        response.assert_status_ok();
        let b: crate::handlers::HealthCheck = response.json();
        assert_eq!(b, crate::handlers::HealthCheck { ok: true });
    }

    #[tokio::test]
    async fn it_authenticates_a_user() {
        let (_core, server) = setup_admin_enabled().await;

        let response = server
            .post("/api/v1/auth")
            .json(&AuthRequest {
                username: "admin".to_owned(),
                password: "wrong".to_owned(),
            })
            .await;
        response.assert_status(StatusCode::UNAUTHORIZED);

        let response = server
            .post("/api/v1/auth")
            .json(&AuthRequest {
                username: "unknown".to_owned(),
                password: "wrong".to_owned(),
            })
            .await;
        response.assert_status(StatusCode::UNAUTHORIZED);

        let response = server
            .post("/api/v1/auth")
            .json(&AuthRequest {
                username: "admin".to_owned(),
                password: "aoeu".to_owned(),
            })
            .await;
        response.assert_status_ok();
        let session_id: Option<SessionId> = response.json();
        assert!(session_id.is_some());
    }

    #[tokio::test]
    async fn it_returns_user_profile() {
        let (_core, server) = setup_admin_enabled().await;

        let response = server.get("/api/v1/user").await;
        response.assert_status(StatusCode::UNAUTHORIZED);

        let response = server
            .post("/api/v1/auth")
            .json(&AuthRequest {
                username: "admin".to_owned(),
                password: "aoeu".to_owned(),
            })
            .await;
        response.assert_status_ok();
        let session_id: Option<SessionId> = response.json();
        let session_id = session_id.unwrap();

        let response = server
            .get("/api/v1/user")
            .add_header("Authorization", format!("Bearer {}", session_id))
            .await;
        response.assert_status_ok();
        let profile: Option<UserProfile> = response.json();
        let profile = profile.unwrap();
        assert_eq!(profile.id, UserId::from("admin"));
        assert_eq!(profile.name, "admin");
    }

    #[tokio::test]
    async fn an_admin_can_create_a_user() {
        // All of the contents of this test are basically required for any test on individual
        // users, so I moved it all into the setup code.
        let (_core, _server) = setup_with_user().await;
    }

    #[tokio::test]
    async fn a_user_can_get_any_user_profile() {
        let (core, server) = setup_with_user().await;

        let savanni = match core.user_by_username("savanni").await {
            ResultExt::Ok(Some(savanni)) => savanni,
            ResultExt::Ok(None) => panic!("user was not initialized"),
            ResultExt::Err(err) => panic!("{:?}", err),
            ResultExt::Fatal(err) => panic!("{:?}", err),
        };

        let response = server
            .post("/api/v1/auth")
            .json(&AuthRequest {
                username: "savanni".to_owned(),
                password: "".to_owned(),
            })
            .await;
        let session_id: Option<SessionId> = response.json();
        let session_id = session_id.unwrap();

        let response = server
            .get(&format!("/api/v1/user/{}", savanni.id))
            .add_header("Authorization", format!("Bearer {}", session_id))
            .await;
        response.assert_status_ok();
        let profile: Option<UserProfile> = response.json();
        let profile = profile.unwrap();
        assert_eq!(profile.name, "savanni");

        let response = server
            .get("/api/v1/user/admin")
            .add_header("Authorization", format!("Bearer {}", session_id))
            .await;
        response.assert_status_ok();
        let profile: Option<UserProfile> = response.json();
        let profile = profile.unwrap();
        assert_eq!(profile.name, "admin");
    }

    #[tokio::test]
    async fn a_user_can_change_their_password() {
        let (_core, server) = setup_with_user().await;

        let response = server
            .post("/api/v1/auth")
            .json(&AuthRequest {
                username: "savanni".to_owned(),
                password: "".to_owned(),
            })
            .await;
        let session_id: Option<SessionId> = response.json();
        let session_id = session_id.unwrap();

        let response = server
            .get("/api/v1/user")
            .add_header("Authorization", format!("Bearer {}", session_id))
            .await;
        let profile = response.json::<Option<UserProfile>>().unwrap();
        assert_eq!(profile.name, "savanni");

        let response = server
            .put("/api/v1/user/password")
            .json(&SetPasswordRequest {
                password_1: "abcdefg".to_owned(),
                password_2: "abcd".to_owned(),
            })
            .await;
        response.assert_status(StatusCode::UNAUTHORIZED);

        let response = server
            .put("/api/v1/user/password")
            .add_header("Authorization", format!("Bearer {}", session_id))
            .json(&SetPasswordRequest {
                password_1: "abcdefg".to_owned(),
                password_2: "abcd".to_owned(),
            })
            .await;
        response.assert_status(StatusCode::BAD_REQUEST);

        let response = server
            .put("/api/v1/user/password")
            .add_header("Authorization", format!("Bearer {}", session_id))
            .json(&SetPasswordRequest {
                password_1: "abcdefg".to_owned(),
                password_2: "abcdefg".to_owned(),
            })
            .await;
        response.assert_status(StatusCode::OK);
    }

    #[tokio::test]
    async fn a_user_can_create_a_game() {
        let (_core, server) = setup_with_user().await;

        let response = server
            .post("/api/v1/auth")
            .json(&AuthRequest {
                username: "savanni".to_owned(),
                password: "".to_owned(),
            })
            .await;
        let session_id = response.json::<Option<SessionId>>().unwrap();

        let response = server
            .put("/api/v1/games")
            .add_header("Authorization", format!("Bearer {}", session_id))
            .json(&CreateGameRequest {
                game_type: "Candela".to_owned(),
                game_name: "Circle of the Winter Solstice".to_owned(),
            })
            .await;
        let _game_id = response.json::<Option<GameId>>().unwrap();
    }

    #[ignore]
    #[tokio::test]
    async fn gms_can_invite_others_into_a_game() {
        unimplemented!();
    }
}