use crate::authentication::Authentication;
#[cfg(test)]
use crate::error::Error as ImageVaultError;
use reqwest::{
    header::HeaderMap, header::HeaderValue, header::USER_AGENT, Client as ReqwestClient,
};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use url::Url;

type BoxedError = Box<dyn std::error::Error>;

const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION");
const PACKAGE_NAME: &'static str = "ImageVault-rs";

/// # Client
///
/// A client that is used to consume the ImageVault API.
///
/// ## Remarks
/// The client saves and mutates the authentication, so it is recommended
/// to re-use the client as much as possible in order to minimize the need
/// to re-authenticate.
pub struct Client<T: Authentication> {
    pub client_identity: String,
    pub client_secret: String,
    pub authentication: Option<Arc<Mutex<T>>>,
    pub reqwest_client: ReqwestClient,
    pub base_url: Url,
}

impl<T: Authentication> Client<T> {
    /// Creates a new `Client`
    ///
    /// ## Arguments
    ///
    /// * `client_identity` - The client identity
    /// * `client_secret` - The client secret
    /// * `base_url` - The base URL to ImageVault, e.g. `https://myimagevault.se`
    pub fn new(
        client_identity: &str,
        client_secret: &str,
        base_url: &str,
    ) -> Result<Self, BoxedError> {
        let client = Client {
            client_identity: client_identity.to_string(),
            client_secret: client_secret.to_string(),
            authentication: None,
            reqwest_client: create_reqwest_client(),
            base_url: Url::from_str(base_url)?,
        };
        Ok(client)
    }

    /// Adds authentication to the Client
    ///
    /// ## Arguments
    ///
    /// * `authentication` - The `Authentication` to use
    ///
    /// ## Remarks
    ///
    /// Pretty much everything in the ImageVault API requires authentication,
    /// so this should be included most of the time.
    ///
    /// ## Examples
    ///
    /// ```
    /// use imagevault::{
    ///     Client,
    ///     authentication::ClientCredentialsAuthentication
    /// };
    ///
    /// # fn test() -> Result<(), Box<dyn std::error::Error>> {
    /// let authentication = ClientCredentialsAuthentication::default();
    ///
    /// let client = Client::new(
    ///     "identity",
    ///     "secret",
    ///     "https://myimagevault.local"
    ///     )?
    ///     .with_authentication(authentication);
    /// # Ok(())
    /// # }
    /// ```
    pub fn with_authentication(mut self, authentication: T) -> Self {
        self.authentication = Some(Arc::new(Mutex::new(authentication)));
        self
    }

    #[cfg(test)]
    pub async fn test_authenticate(&self) -> Result<String, BoxedError> {
        let auth_unwrapped = self
            .authentication
            .as_ref()
            .ok_or_else(|| ImageVaultError::AuthenticationMissing)?;
        auth_unwrapped
            .lock()
            .unwrap()
            .authenticate(
                &self.client_identity,
                &self.client_secret,
                &self.base_url,
                &self.reqwest_client,
            )
            .await
    }
}

/// Create a new reqwest::client with default headers.
fn create_reqwest_client() -> ReqwestClient {
    // default headers for requests
    let user_agent_value = format!("{}/{}", PACKAGE_NAME, PACKAGE_VERSION);
    let mut headers = HeaderMap::new();
    headers.insert(
        USER_AGENT,
        HeaderValue::from_str(&user_agent_value).unwrap_or(HeaderValue::from_static(PACKAGE_NAME)),
    );
    ReqwestClient::builder()
        .default_headers(headers)
        .build()
        .unwrap_or(ReqwestClient::new())
}

#[cfg(test)]
mod tests {
    use crate::authentication::DummyAuth;
    use crate::Client;
    #[test]
    fn create_new_client() {
        let client_ok: Result<Client<DummyAuth>, Box<dyn std::error::Error>> = Client::new(
            "client_identifier",
            "client_secret",
            "https://imagevaultbaseurl.local",
        );
        let client_bad_url: Result<Client<DummyAuth>, Box<dyn std::error::Error>> =
            Client::new("client_identifier", "client_secret", "asjdlkjasd");
        let client_empty_url: Result<Client<DummyAuth>, Box<dyn std::error::Error>> =
            Client::new("client_identifier", "client_secret", "");
        assert!(client_ok.is_ok());
        assert!(client_bad_url.is_err());
        assert!(client_empty_url.is_err());
    }
}
