use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Serialize;

#[derive(Debug, Serialize)]
struct AwsErrorBody {
    #[serde(rename = "__type")]
    error_type: String,
    message: String,
}

#[derive(Debug)]
#[allow(dead_code)]
pub enum CognitoError {
    ResourceNotFound(String),
    UserNotFound(String),
    UsernameExists(String),
    NotAuthorized(String),
    InvalidParameter(String),
    InvalidPassword(String),
    CodeMismatch(String),
    ExpiredCode(String),
    InternalError(String),
}

impl CognitoError {
    fn type_and_status(&self) -> (&str, StatusCode) {
        match self {
            Self::ResourceNotFound(_) => ("ResourceNotFoundException", StatusCode::BAD_REQUEST),
            Self::UserNotFound(_) => ("UserNotFoundException", StatusCode::BAD_REQUEST),
            Self::UsernameExists(_) => ("UsernameExistsException", StatusCode::BAD_REQUEST),
            Self::NotAuthorized(_) => ("NotAuthorizedException", StatusCode::BAD_REQUEST),
            Self::InvalidParameter(_) => ("InvalidParameterException", StatusCode::BAD_REQUEST),
            Self::InvalidPassword(_) => ("InvalidPasswordException", StatusCode::BAD_REQUEST),
            Self::CodeMismatch(_) => ("CodeMismatchException", StatusCode::BAD_REQUEST),
            Self::ExpiredCode(_) => ("ExpiredCodeException", StatusCode::BAD_REQUEST),
            Self::InternalError(_) => ("InternalErrorException", StatusCode::INTERNAL_SERVER_ERROR),
        }
    }

    fn message(&self) -> &str {
        match self {
            Self::ResourceNotFound(m) | Self::UserNotFound(m) | Self::UsernameExists(m)
            | Self::NotAuthorized(m) | Self::InvalidParameter(m) | Self::InvalidPassword(m)
            | Self::CodeMismatch(m) | Self::ExpiredCode(m) | Self::InternalError(m) => m,
        }
    }
}

impl IntoResponse for CognitoError {
    fn into_response(self) -> Response {
        let (error_type, status) = self.type_and_status();
        let body = AwsErrorBody {
            error_type: error_type.to_string(),
            message: self.message().to_string(),
        };
        (status, axum::Json(body)).into_response()
    }
}

impl std::fmt::Display for CognitoError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let (error_type, _) = self.type_and_status();
        write!(f, "{}: {}", error_type, self.message())
    }
}

impl std::error::Error for CognitoError {}

#[cfg(test)]
mod tests {
    use super::*;
    use http_body_util::BodyExt;

    #[tokio::test]
    async fn test_error_response_format() {
        let err = CognitoError::UserNotFound("User does not exist.".to_string());
        let response = err.into_response();
        assert_eq!(response.status(), StatusCode::BAD_REQUEST);

        let body = response.into_body().collect().await.unwrap().to_bytes();
        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(json["__type"], "UserNotFoundException");
        assert_eq!(json["message"], "User does not exist.");
    }

    #[tokio::test]
    async fn test_internal_error_is_500() {
        let err = CognitoError::InternalError("something broke".to_string());
        let response = err.into_response();
        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
    }
}
