//! Error types of the distribution spec.

use crate::error::OciSpecError;
use derive_builder::Builder;
use getset::Getters;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter};
use strum_macros::{Display as StrumDisplay, EnumString};
use thiserror::Error;

/// The string returned by and ErrorResponse error.
pub const ERR_REGISTRY: &str = "distribution: registry returned error";

/// Unique identifier representing error code.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, StrumDisplay, EnumString)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
    /// Blob unknown to registry.
    BlobUnknown,
    /// Blob upload invalid.
    BlobUploadInvalid,
    /// Blob upload unknown to registry.
    BlobUploadUnknown,
    /// Provided digest did not match uploaded content.
    DigestInvalid,
    /// Blob unknown to registry.
    ManifestBlobUnknown,
    /// Manifest invalid.
    ManifestInvalid,
    /// Manifest unknown.
    ManifestUnknown,
    /// Invalid repository name.
    NameInvalid,
    /// Repository name not known to registry.
    NameUnknown,
    /// Provided length did not match content length.
    SizeInvalid,
    /// Authentication required.
    Unauthorized,
    /// Requested access to the resource is denied.
    Denied,
    /// The operation is unsupported.
    Unsupported,
    /// Too many requests.
    #[serde(rename = "TOOMANYREQUESTS")]
    TooManyRequests,
}

#[derive(Builder, Clone, Debug, Deserialize, Eq, Error, Getters, PartialEq, Serialize)]
#[builder(
    pattern = "owned",
    setter(into, strip_option),
    build_fn(error = "OciSpecError")
)]
#[getset(get = "pub")]
/// ErrorResponse is returned by a registry on an invalid request.
pub struct ErrorResponse {
    /// Available errors within the response.
    errors: Vec<ErrorInfo>,
}

impl Display for ErrorResponse {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{ERR_REGISTRY}")
    }
}

impl ErrorResponse {
    /// Returns the ErrorInfo slice for the response.
    pub fn detail(&self) -> &[ErrorInfo] {
        &self.errors
    }
}

#[derive(Builder, Clone, Debug, Deserialize, Eq, Getters, PartialEq, Serialize)]
#[builder(
    pattern = "owned",
    setter(into, strip_option),
    build_fn(error = "OciSpecError")
)]
#[getset(get = "pub")]
/// Describes a server error returned from a registry.
pub struct ErrorInfo {
    /// The code field MUST be a unique identifier, containing only uppercase alphabetic
    /// characters and underscores.
    code: ErrorCode,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[builder(default = "None")]
    /// The message field is OPTIONAL, and if present, it SHOULD be a human readable string or
    /// MAY be empty.
    message: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none", with = "json_string")]
    #[builder(default = "None")]
    /// The detail field is OPTIONAL and MAY contain arbitrary JSON data providing information
    /// the client can use to resolve the issue.
    detail: Option<String>,
}

mod json_string {
    use std::str::FromStr;

    use serde::{Deserialize, Deserializer, Serialize, Serializer};

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
    where
        D: Deserializer<'de>,
    {
        use serde::de::Error;

        let opt = Option::<serde_json::Value>::deserialize(deserializer)?;

        if let Some(data) = opt {
            let data = serde_json::to_string(&data).map_err(Error::custom)?;
            return Ok(Some(data));
        }

        Ok(None)
    }

    pub fn serialize<S>(target: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        use serde::ser::Error;

        match target {
            Some(data) => {
                if let Ok(json_value) = serde_json::Value::from_str(data) {
                    json_value.serialize(serializer)
                } else {
                    Err(Error::custom("invalid JSON"))
                }
            }
            _ => unreachable!(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::Result;

    #[test]
    fn error_response_success() -> Result<()> {
        let response = ErrorResponseBuilder::default().errors(vec![]).build()?;
        assert!(response.detail().is_empty());
        assert_eq!(response.to_string(), ERR_REGISTRY);
        Ok(())
    }

    #[test]
    fn error_response_failure() {
        assert!(ErrorResponseBuilder::default().build().is_err());
    }

    #[test]
    fn error_info_success() -> Result<()> {
        let info = ErrorInfoBuilder::default()
            .code(ErrorCode::BlobUnknown)
            .build()?;
        assert_eq!(info.code(), &ErrorCode::BlobUnknown);
        assert!(info.message().is_none());
        assert!(info.detail().is_none());
        Ok(())
    }

    #[test]
    fn error_info_failure() {
        assert!(ErrorInfoBuilder::default().build().is_err());
    }

    #[test]
    fn error_info_serialize_success() -> Result<()> {
        let error_info = ErrorInfoBuilder::default()
            .code(ErrorCode::Unauthorized)
            .detail(String::from("{ \"key\": \"value\" }"))
            .build()?;

        assert!(serde_json::to_string(&error_info).is_ok());
        Ok(())
    }

    #[test]
    fn error_info_serialize_failure() -> Result<()> {
        let error_info = ErrorInfoBuilder::default()
            .code(ErrorCode::Unauthorized)
            .detail(String::from("abcd"))
            .build()?;

        assert!(serde_json::to_string(&error_info).is_err());
        Ok(())
    }

    #[test]
    fn error_info_deserialize_success() -> Result<()> {
        let error_info_str = r#"
        {
            "code": "MANIFEST_UNKNOWN",
            "message": "manifest tagged by \"lates\" is not found",
            "detail": {
                "Tag": "lates"
            }
        }"#;

        let error_info: ErrorInfo = serde_json::from_str(error_info_str)?;
        assert_eq!(error_info.detail().as_ref().unwrap(), "{\"Tag\":\"lates\"}");

        Ok(())
    }
}
