use crate::authentication::Authentication;
use crate::error::Error;
use crate::Client;
use bytesize::ByteSize;
use reqwest::header::CONTENT_TYPE;
use std::fs::{metadata, File};
use std::io::Read;
use std::path::Path;

type BoxedError = Box<dyn std::error::Error>;
const UPLOAD_ENDPOINT: &'static str = "/apiv2/uploadservice/upload";
const UPLOAD_CHUNK_SIZE: u64 = 50; // 50 kiB chunk size

/// Calls `uploadservice/upload`.
///
/// The file pointed to by `path` will be uploaded in 50 kiB chunks.
///
/// Returns an `AuthenticationMissing` error
/// if the `Client` does not have any `Authentication` set.
///
/// ## Arguments
/// * `client` - The ImageVault `Client` to use.
/// * `path` - The path to the file to upload.
///
/// ## Examples
///
/// ```
/// use std::path::Path;
/// use imagevault::{
///     service::upload_service,
///     Client,
///     authentication::ClientCredentialsAuthentication
/// };
///
/// # async fn test() -> Result<(), Box<dyn std::error::Error>> {
/// let authentication = ClientCredentialsAuthentication::default();
/// let client = Client::new(
///     "identity",
///     "secret",
///     "https://myimagevault.local"
///     )?
///     .with_authentication(authentication);
///
/// let file_path = Path::new("my_image.jpg");
/// let upload_file_id = upload_service::upload_from_path(
///     &client,
///     file_path
///     )
///     .await?;
/// # Ok(())
/// # }
/// ```
pub async fn upload_from_path<T: Authentication>(
    client: &Client<T>,
    path: &Path,
) -> Result<String, BoxedError> {
    let _md = metadata(path)?;
    let file = File::open(path)?;
    upload_from_content(client, &file).await
}

/// Calls `uploadservice/upload`.
///
/// Returns an `AuthenticationMissing` error
/// if the `Client` does not have any `Authentication` set.
///
/// ## Arguments
/// * `client` - The ImageVault `Client` to use.
/// * `content` - Any `Read` data to be uploaded, e.g. a file.
///
/// ## Examples
///
/// ```
/// use std::path::Path;
/// use std::fs::File;
/// use imagevault::{
///     service::upload_service,
///     Client,
///     authentication::ClientCredentialsAuthentication
/// };
///
/// # async fn test() -> Result<(), Box<dyn std::error::Error>> {
/// let authentication = ClientCredentialsAuthentication::default();
/// let client = Client::new(
///     "identity",
///     "secret",
///     "https://myimagevault.local"
///     )?
///     .with_authentication(authentication);
///
/// let file_path = Path::new("my_image.jpg");
/// let file = File::open(file_path)?;
/// let upload_file_id = upload_service::upload_from_content(
///     &client,
///     &file
///     )
///     .await?;
/// # Ok(())
/// # }
/// ```
pub async fn upload_from_content<T: Authentication, P: Read>(
    client: &Client<T>,
    mut content: P,
) -> Result<String, BoxedError> {
    let chunk_size = ByteSize::kib(UPLOAD_CHUNK_SIZE).as_u64();
    let mut file_id: Option<String> = None;
    loop {
        let data: Vec<u8> = content
            .by_ref()
            .bytes()
            .take(chunk_size as usize)
            .map(|b| b.unwrap())
            .collect();
        let data_read = data.len();

        // No more data to upload, we are done
        if data_read < 1 {
            break;
        }

        // Upload
        let full_url = match file_id {
            Some(id) => client
                .base_url
                .join(&format!("{}/{}", UPLOAD_ENDPOINT, id))?,
            None => client.base_url.join(UPLOAD_ENDPOINT)?,
        };

        let auth_unwrapped = client
            .authentication
            .as_ref()
            .ok_or_else(|| Error::AuthenticationMissing)?;
        let auth_header = auth_unwrapped
            .lock()
            .unwrap()
            .authenticate(
                &client.client_identity,
                &client.client_secret,
                &client.base_url,
                &client.reqwest_client,
            )
            .await?;

        let response = client
            .reqwest_client
            .post(full_url)
            .header(CONTENT_TYPE, "application/octet-stream")
            .bearer_auth(auth_header)
            .body(data)
            .send()
            .await?;
        if let Err(err) = response.error_for_status_ref() {
            return Err(Box::new(err));
        } else {
            file_id = Some(response.json::<String>().await?);
        }
    }

    // Return result
    Ok(file_id.unwrap_or(String::from("")))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::authentication::DummyAuth;
    use crate::testutil;
    use crate::testutil::get_test_data;
    use std::path::Path;

    const UPLOAD_SECOND: &'static str = "/apiv2/uploadservice/upload/mn_kEJlt24v_CQ_p1XSL";
    #[test]
    fn from_path_not_existing_test() {
        let mut rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let authentication = DummyAuth::new();
            let client = Client::new(
                "client_identity",
                "client_secret",
                "https://myimagevault.se",
            )
            .unwrap()
            .with_authentication(authentication);
            let p1 = Path::new("this/should/not/exist/asdjalksdjlkasdj");
            let _res = upload_from_path(&client, p1).await;
            assert!(_res.is_err());
        });
    }

    #[test]
    fn from_path_upload_image_test() {
        let mut rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let mock_first = mockito::mock("POST", UPLOAD_ENDPOINT)
                .expect(1)
                .with_status(200)
                .with_header("content-type", "application/json")
                .with_body(get_test_data("upload_result"))
                .create();
            let mock_second = mockito::mock("POST", UPLOAD_SECOND)
                .expect(2)
                .with_status(200)
                .with_header("content-type", "application/json")
                .with_body(get_test_data("upload_result"))
                .create();
            let authentication = DummyAuth::new();
            let client = Client::new("client_identity", "client_secret", &mockito::server_url())
                .unwrap()
                .with_authentication(authentication);
            let gif_path = testutil::get_gif_test_image_path();
            let gif_res = upload_from_path(&client, &gif_path).await;
            assert!(gif_res.is_ok());
            mock_first.assert();
            mock_second.assert();
        });
    }
}
