use crate::databases::{CredentialsDatabase, TokensDatabase};

use super::{AuthService, Credentials};

pub struct AuthServiceImpl<C, T> {
    pub credentials_db: C,
    pub tokens_db: T,
}

#[async_trait::async_trait]
impl<C, T> AuthService for AuthServiceImpl<C, T>
where
    C: CredentialsDatabase + Send + Sync,
    T: TokensDatabase + Send + Sync,
{
    async fn register(&self, credentials: &Credentials) -> bool {
        log::info!("Registering");
        self.credentials_db
            .save_credentials(credentials)
            .await
            .is_ok()
    }

    async fn login(&self, credentials: &Credentials) -> Option<String> {
        log::info!("Logging in ");
        if self.credentials_db.do_exist(credentials).await.ok()? {
            log::debug!("Generating token");
            self.tokens_db.store_user(&credentials.username).await.ok()
        } else {
            log::debug!("User does not exist");
            None
        }
    }

    async fn authenticate(&self, token: &str) -> Option<String> {
        log::info!("Authenticating");
        self.tokens_db
            .get_username(token)
            .await
            .map_err(|e| {
                log::error!("Could not receive user by its token: {}", e);
                e
            })
            .ok()
    }
}

#[cfg(test)]
mod should {
    use mockall::predicate::eq;

    use crate::databases::{MockCredentialsDatabase, MockTokensDatabase};

    use super::*;

    #[tokio::test]
    async fn save_new_credentials_to_database() {
        let mut credentials_db = MockCredentialsDatabase::new();
        credentials_db
            .expect_save_credentials()
            .with(eq(Credentials {
                username: "user".into(),
                password: "password".into(),
            }))
            .returning(|_| Ok(()));
        let token_db = MockTokensDatabase::new();

        let sut = AuthServiceImpl {
            credentials_db,
            tokens_db: token_db,
        };

        let result = sut
            .register(&Credentials {
                username: "user".into(),
                password: "password".into(),
            })
            .await;

        assert_eq!(result, true);
    }

    #[tokio::test]
    async fn return_false_on_database_error() {
        let mut credentials_db = MockCredentialsDatabase::new();
        credentials_db
            .expect_save_credentials()
            .returning(|_| Err(anyhow::anyhow!("Database error")));
        let token_db = MockTokensDatabase::new();

        let sut = AuthServiceImpl {
            credentials_db,
            tokens_db: token_db,
        };

        let result = sut
            .register(&Credentials {
                username: "user".into(),
                password: "password".into(),
            })
            .await;

        assert_eq!(result, false);
    }

    #[tokio::test]
    async fn generate_token_if_credentials_exist_in_database() {
        let mut credentials_db = MockCredentialsDatabase::new();
        credentials_db
            .expect_do_exist()
            .once()
            .with(eq(Credentials {
                username: "user".into(),
                password: "password".into(),
            }))
            .returning(|_| Ok(true));

        let mut token_db = MockTokensDatabase::new();
        token_db
            .expect_store_user()
            .with(eq("user"))
            .once()
            .returning(|_| Ok("token".into()));

        let sut = AuthServiceImpl {
            credentials_db,
            tokens_db: token_db,
        };

        let result = sut
            .login(&Credentials {
                username: "user".into(),
                password: "password".into(),
            })
            .await;

        assert_eq!(result, Some("token".into()));
    }

    #[tokio::test]
    async fn do_not_login_on_missing_credentials() {
        let mut credentials_db = MockCredentialsDatabase::new();
        credentials_db
            .expect_do_exist()
            .once()
            .with(eq(Credentials {
                username: "user".into(),
                password: "password".into(),
            }))
            .returning(|_| Ok(false));

        let token_db = MockTokensDatabase::new();

        let sut = AuthServiceImpl {
            credentials_db,
            tokens_db: token_db,
        };

        let result = sut
            .login(&Credentials {
                username: "user".into(),
                password: "password".into(),
            })
            .await;

        assert_eq!(result, None);
    }

    #[tokio::test]
    async fn return_username_for_token() {
        let credentials_db = MockCredentialsDatabase::new();

        let mut token_db = MockTokensDatabase::new();
        token_db
            .expect_get_username()
            .once()
            .with(eq("token".into()))
            .returning(|_| Ok("user".into()));

        let sut = AuthServiceImpl {
            credentials_db,
            tokens_db: token_db,
        };

        let result = sut.authenticate("token").await;

        assert_eq!(result, Some("user".into()));
    }

    #[tokio::test]
    async fn do_not_propagate_tokens_db_error() {
        let credentials_db = MockCredentialsDatabase::new();

        let mut token_db = MockTokensDatabase::new();
        token_db
            .expect_get_username()
            .once()
            .returning(|_| Err(anyhow::anyhow!("some error")));

        let sut = AuthServiceImpl {
            credentials_db,
            tokens_db: token_db,
        };

        let result = sut.authenticate("token").await;

        assert_eq!(result, None);
    }
}
