use crate::{authentication::Authentication, data::AssetsSearchResult, error::Error, Client};
use serde::Serialize;

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

const SEARCH_ENDPOINT: &'static str = "apiv2/assets";

/// A builder for the `assets/` search endpoint
pub struct SearchBuilder {
    pub search: String,
    filter: Option<String>,
    order_by: Option<String>,
    skip: Option<i64>,
    top: Option<i64>,
    count: Option<bool>,
    select: Option<String>,
    facet: Option<String>,
}

impl SearchBuilder {
    /// Creates a new builder.
    pub fn new(search: &str) -> Self {
        SearchBuilder {
            search: search.to_string(),
            filter: None,
            order_by: None,
            skip: None,
            top: None,
            count: None,
            select: None,
            facet: None,
        }
    }

    /// Adds the `$filter` query to the search
    pub fn filter(mut self, filter: &str) -> Self {
        self.filter = Some(filter.into());
        self
    }

    /// Adds the `$orderby` query to the search
    pub fn order_by(mut self, order_by: &str) -> Self {
        self.order_by = Some(order_by.to_string());
        self
    }

    /// Adds the `$skip` query to the search
    pub fn skip(mut self, skip: i64) -> Self {
        self.skip = Some(skip);
        self
    }

    /// Adds the `$top` query to the search
    pub fn top(mut self, top: i64) -> Self {
        self.top = Some(top);
        self
    }

    /// Adds the `$count` query to the search
    pub fn count(mut self, count: bool) -> Self {
        self.count = Some(count);
        self
    }

    /// Adds the `$select` query to the search
    pub fn select(mut self, select: &str) -> Self {
        self.select = Some(select.to_string());
        self
    }

    /// Adds the `$facet` query to the search
    pub fn facet(mut self, facet: &str) -> Self {
        self.facet = Some(facet.to_string());
        self
    }

    /// Finalizes the `SearchQuery` to use with `assets/` search endpoint
    pub fn build(self) -> SearchQuery {
        SearchQuery::from_builder(self)
    }
}

#[derive(Serialize, Debug)]
/// Search query for the `/assets` endpoint.
/// Constructed using `SearchQueryBuilder`
pub struct SearchQuery {
    #[serde(rename = "$search")]
    search: String,
    #[serde(rename = "$filter")]
    filter: Option<String>,
    #[serde(rename = "$orderby")]
    order_by: Option<String>,
    #[serde(rename = "$skip")]
    skip: Option<i64>,
    #[serde(rename = "$top")]
    top: Option<i64>,
    #[serde(rename = "$count")]
    count: Option<bool>,
    #[serde(rename = "$select")]
    select: Option<String>,
    #[serde(rename = "$facet")]
    facet: Option<String>,
}

impl SearchQuery {
    fn from_builder(builder: SearchBuilder) -> Self {
        SearchQuery {
            search: builder.search,
            filter: builder.filter,
            order_by: builder.order_by,
            skip: builder.skip,
            top: builder.top,
            count: builder.count,
            select: builder.select,
            facet: builder.facet,
        }
    }
}

/// Calls the `assets/` search endpoint.
///
/// Returns an `AuthenticationMissing` error
/// if the `Client` does not have any `Authentication` set.
///
/// ## Arguments
/// * `client` - The ImageVault `Client` to use.
/// * `search_query` - The `SearchQuery` to use.
///
/// ## Examples
///
/// ```
/// use imagevault::{
///     service::assets,
///     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 query = assets::SearchBuilder::new("cat")
///     .filter("isOrganized")
///     .top(20)
///     .build();
/// let search_result = assets::search(&client, &query).await?;
/// # Ok(())
/// # }
/// ```
pub async fn search<T: Authentication>(
    client: &Client<T>,
    search_query: &SearchQuery,
) -> Result<AssetsSearchResult, BoxedError> {
    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 full_url = client.base_url.join(SEARCH_ENDPOINT)?;

    let response = client
        .reqwest_client
        .get(full_url)
        .query(search_query)
        .bearer_auth(auth_header)
        .send()
        .await?;
    if let Err(err) = response.error_for_status_ref() {
        return Err(Box::new(err));
    } else {
        let result = response.json::<AssetsSearchResult>().await?;
        return Ok(result);
    }
}

#[cfg(test)]
mod tests {
    use crate::testutil::get_test_data;
    use crate::{authentication::DummyAuth, service::assets, Client};
    #[test]
    fn assets_search_test() {
        let mut rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            // setup mock HTTP response
            let mock = mockito::mock("GET", mockito::Matcher::Any)
                .expect(1)
                .with_status(200)
                .with_header("content-type", "application/json")
                .with_body(get_test_data("assets_search_result"))
                .create();
            let authentication = DummyAuth::new();
            let client = Client::new("client_identity", "client_secret", &mockito::server_url())
                .unwrap()
                .with_authentication(authentication);
            let query = assets::SearchBuilder::new("")
                .top(100)
                .filter("isOrganized and vaultId eq 2")
                .build();
            let search_result = assets::search(&client, &query).await;
            assert!(search_result.is_ok());
            mock.assert();
        });
    }
}
