#![deny(missing_docs)]
#![forbid(rustdoc::broken_intra_doc_links)]
//! A library of useful structs and helpers to implement [IndieAuth
//! version 20220212][indieauth].
//!
//! This crate is completely network-agnostic, which means it can be
//! used with both sync and async web frameworks, and even on the
//! client side to implement identity consumers.
//!
//! ## Integration with web frameworks
//!
//! For convenience, helpers for [`axum`], the web framework Kittybox
//! happens to use, are provided. Enable the `axum` feature to use
//! them.
//!
//! The author is happy to accept patches to add more
//! framework-specific helpers.
//!
//! [indieauth]: https://indieauth.spec.indieweb.org/20220212/
//! [`axum`]: https://github.com/tokio-rs/axum
use serde::{Serialize, Deserialize};
use url::Url;

mod scopes;
pub use self::scopes::{Scope, Scopes};
mod pkce;
pub use self::pkce::{PKCEMethod, PKCEVerifier, PKCEChallenge};

/// Authentication methods supported by the introspection endpoint.
/// Note that authentication at the introspection endpoint is
/// mandatory.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub enum IntrospectionEndpointAuthMethod {
    /// `Authorization` header with a `Bearer` token.
    Bearer,
    /// A token passed as part of a POST request.
    #[serde(rename = "snake_case")]
    ClientSecretPost,
    /// Username and password passed using HTTP Basic authentication.
    #[serde(rename = "snake_case")]
    ClientSecretBasic,
    /// TLS client auth with a certificate signed by a valid CA.
    #[serde(rename = "snake_case")]
    TlsClientAuth,
    /// TLS client auth with a self-signed certificate.
    #[serde(rename = "snake_case")]
    SelfSignedTlsClientAuth
}

/// Authentication methods supported by the revocation endpoint.
///
/// The intent of the IndieAuth revocation endpoints is to quickly
/// revoke leaked tokens. As it requires posession of a token, no
/// authentication is neccesary to protect tokens. A well-intentioned
/// person discovering a leaked token could quickly revoke it without
/// disturbing anyone.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RevocationEndpointAuthMethod {
    /// No authentication is required to access an endpoint declaring
    /// this value.
    None
}

/// The response types supported by the authorization endpoint.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseType {
    /// An authorization code will be issued if this response type is
    /// requested.
    Code
}
// TODO serde_variant
impl ResponseType {
    /// Return the response type as it would appear in serialized form.
    pub fn as_str(&self) -> &'static str {
        match self {
            ResponseType::Code => "code",
        }
    }
}

/// Grant types that are described in the IndieAuth spec.
///
/// This type is strictly for usage in the [`Metadata`] response. For
/// grant requests and responses, see [`GrantRequest`] and
/// [`GrantResponse`].
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GrantType {
    /// The authorization code grant, allowing to exchange an
    /// authorization code for a confirmation of identity or an access
    /// token.
    AuthorizationCode,
    /// The refresh token grant, allowing to exchange a refresh token
    /// for a fresh access token and a new refresh token, to
    /// facilitate long-term access.
    RefreshToken
}

/// OAuth 2.0 Authorization Server Metadata in application to the IndieAuth protocol.
///
/// Your metadata endpoint should return this as a response.
///
/// ```rust
/// use kittybox_indieauth::{
///     Metadata, IntrospectionEndpointAuthMethod, RevocationEndpointAuthMethod,
///     ResponseType, Scope, GrantType, PKCEMethod
/// };
///
/// let metadata = Metadata {
///     issuer: "https://indieauth.example.com/".parse().unwrap(),
///     authorization_endpoint: "https://indieauth.example.com/auth".parse().unwrap(),
///     token_endpoint: "https://indieauth.example.com/token".parse().unwrap(),
///     introspection_endpoint: "https://indieauth.example.com/introspection".parse().unwrap(),
///     introspection_endpoint_auth_methods_supported: Some(vec![IntrospectionEndpointAuthMethod::Bearer]),
///     revocation_endpoint: Some("https://indieauth.example.com/revoke".parse().unwrap()),
///     revocation_endpoint_auth_methods_supported: Some(vec![RevocationEndpointAuthMethod::None]),
///     scopes_supported: Some(vec![Scope::Create, Scope::Update, Scope::custom("manage_tokens")]),
///     response_types_supported: Some(vec![ResponseType::Code]),
///     grant_types_supported: Some(vec![GrantType::AuthorizationCode, GrantType::RefreshToken]),
///     service_documentation: Some("https://indieauth.spec.indieweb.org/".parse().unwrap()),
///     code_challenge_methods_supported: vec![PKCEMethod::S256],
///     authorization_response_iss_parameter_supported: Some(true),
///     userinfo_endpoint: Some("https://indieauth.example.com/userinfo".parse().unwrap())
/// };
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
    /// The server's issuer identifier. The issuer identifier is a URL
    /// that uses the "https" scheme and has no query or fragment
    /// components. The identifier MUST be a prefix of the
    /// `indieauth-metadata` URL.
    pub issuer: Url,
    /// The Authorization Endpoint
    pub authorization_endpoint: Url,
    /// The Token Endpoint
    pub token_endpoint: Url,
    /// The Introspection Endpoint
    pub introspection_endpoint: Url,
    /// JSON array containing a list of client authentication methods
    /// supported by this introspection endpoint.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub introspection_endpoint_auth_methods_supported: Option<Vec<IntrospectionEndpointAuthMethod>>,
    /// The Revocation Endpoint
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revocation_endpoint: Option<Url>,
    /// JSON array containing the value
    /// [`RevocationEndpointAuthMethod::None`]. If a revocation endpoint
    /// is provided, this property should also be provided with the
    /// value `vec![RevocationEndpointAuthMethod::None]`, since the
    /// omission of this value defaults to `client_secret_basic`
    /// according to [RFC8414].
    ///
    /// [RFC8414]: https://www.rfc-editor.org/rfc/rfc8414
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revocation_endpoint_auth_methods_supported: Option<Vec<RevocationEndpointAuthMethod>>,
    /// JSON array containing scope values supported by the IndieAuth
    /// server. Servers MAY choose not to advertise some supported
    /// scope values even when this parameter is used.
    // Note: Scopes isn't used here because this field should be
    // serialized as a list, not as a string
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scopes_supported: Option<Vec<Scope>>,
    /// JSON array containing the response_type values supported. This
    /// differs from [RFC8414] in that this parameter is OPTIONAL and
    /// that, if omitted, the default is [`ResponseType::Code`].
    ///
    /// [RFC8414]: https://www.rfc-editor.org/rfc/rfc8414
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_types_supported: Option<Vec<ResponseType>>,
    /// JSON array containing grant type values supported. If omitted,
    /// the default value differs from [RFC8414] and is
    /// `authorization_code`.
    ///
    /// [RFC8414]: https://www.rfc-editor.org/rfc/rfc8414
    #[serde(skip_serializing_if = "Option::is_none")]
    pub grant_types_supported: Option<Vec<GrantType>>,
    /// URL of a page containing human-readable information that
    /// developers might need to know when using the server. This
    /// might be a link to the IndieAuth spec or something more
    /// personal to your implementation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub service_documentation: Option<Url>,
    /// JSON array containing the methods supported for PKCE. This
    /// parameter differs from [RFC8414] in that it is not optional as
    /// PKCE is *REQUIRED*.
    ///
    /// [RFC8414]: https://www.rfc-editor.org/rfc/rfc8414
    pub code_challenge_methods_supported: Vec<PKCEMethod>,
    /// Boolean parameter indicating whether the authorization server
    /// provides the iss parameter. If omitted, the default value is
    /// false. As the iss parameter is REQUIRED, this is provided for
    /// compatibility with OAuth 2.0 servers implementing the
    /// parameter.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub authorization_response_iss_parameter_supported: Option<bool>,
    /// The User Info Endpoint
    #[serde(skip_serializing_if = "Option::is_none")]
    pub userinfo_endpoint: Option<Url>
}

#[cfg(feature = "axum")]
impl axum_core::response::IntoResponse for Metadata {
    fn into_response(self) -> axum_core::response::Response {
        use http::StatusCode;

        (StatusCode::OK,
         [("Content-Type", "application/json")],
         serde_json::to_vec(&self).unwrap())
            .into_response()
    }
}

/// User profile to be returned from the userinfo endpoint and when
/// the `profile` scope was requested.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Profile {
    /// User's chosen name.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// User's profile page. Fetching it may reveal an `h-card`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<Url>,
    /// User's profile picture suitable to represent them.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub photo: Option<Url>,
    /// User's email, if they've chosen to reveal it. This is guarded
    /// by the `email` scope.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub email: Option<String>
}

#[cfg(feature = "axum")]
impl axum_core::response::IntoResponse for Profile {
    fn into_response(self) -> axum_core::response::Response {
        use http::StatusCode;

        (StatusCode::OK,
         [("Content-Type", "application/json")],
         serde_json::to_vec(&self).unwrap())
            .into_response()
    }
}

/// A state string comprised of alphanumeric characters to protect
/// from CSRF attacks.
///
/// There is no reason to inspect the string itself except to ensure
/// it hasn't been tampered with.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct State(String);
impl State {
    /// Generate a random state string of 128 bytes in length.
    pub fn new() -> Self {
        use rand::{Rng, distributions::Alphanumeric};
        let bytes = rand::thread_rng()
            .sample_iter(&Alphanumeric)
            .take(128)
            .collect::<Vec<u8>>();
        Self(String::from_utf8(bytes).unwrap())
    }
}
impl AsRef<str> for State {
    fn as_ref(&self) -> &str {
        self.0.as_str()
    }
}

/// The authorization request that should be affixed to the URL of an
/// authorization endpoint to start the IndieAuth ceremony.
///
/// ```rust
/// use kittybox_indieauth::{
///     AuthorizationRequest, ResponseType, State,
///     Scopes, Scope,
///     PKCEChallenge, PKCEVerifier, PKCEMethod
/// };
///
/// // Save that for later, it'll come in handy
/// let verifier = PKCEVerifier::new();
///
/// let request = AuthorizationRequest {
///     response_type: ResponseType::Code,
///     client_id: "https://kittybox.fireburn.ru/companion/native".parse().unwrap(),
///     redirect_uri: "https://kittybox.fireburn.ru/companion/native/redirect".parse().unwrap(),
///     state: State::new(),
///     code_challenge: PKCEChallenge::new(&verifier, PKCEMethod::default()),
///     scope: Some(Scopes::new(vec![Scope::Create, Scope::Update, Scope::Delete, Scope::Media])),
///     me: Some("https://fireburn.ru/".parse().unwrap())
/// };
///
/// let mut url: url::Url = "https://fireburn.ru/.kittybox/indieauth/auth"
///     .parse()
///     .unwrap();
///
/// url.set_query(Some(&serde_urlencoded::to_string(request).unwrap()));
///
/// // Open a user's browser to navigate to the authorization endpoint page...
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorizationRequest {
    /// The response type expected to this request.
    pub response_type: ResponseType,
    /// The homepage of the client. It must be fetched to show
    /// metadata and check the redirect URI's authenticity.
    pub client_id: Url,
    /// The URI that the user will be redirected to in case they
    /// approve the authentication request. A query string containing
    /// the response is affixed to it.
    pub redirect_uri: Url,
    /// A random state to protect from CSRF attacks. The server should
    /// return this string unmodified.
    pub state: State,
    /// A PKCE challenge neccesary to protect from authorization code
    /// injection and CSRF attacks.
    #[serde(flatten)]
    pub code_challenge: PKCEChallenge,
    /// An array of scopes that are requested for a token. If no
    /// scopes are provided, a token will not be issued.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scope: Option<Scopes>,
    /// The URL that user entered. The authorization endpoint MAY use
    /// it as a hint of which user is attempting to sign in, and to
    /// indicate which profile URL the client is expecting in the
    /// resulting profile URL response or access token response.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub me: Option<Url>
}

/// The authorization response that must be appended to the
/// [`AuthorizationRequest::redirect_uri`]'s query string.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorizationResponse {
    /// The authorization code generated by the authorization
    /// endpoint. The code MUST expire shortly after it is issued to
    /// mitigate the risk of leaks, and MUST be valid for only one
    /// use. A maximum lifetime of 10 minutes is recommended. See
    /// [OAuth 2.0 Section 4.1.2][oauth2-sec-4.1.2] for additional
    /// requirements on the authorization code.
    ///
    /// [oauth2-sec-4.1.2]: https://tools.ietf.org/html/rfc6749#section-4.1.2
    pub code: String,
    /// The state parameter from the [AuthorizationRequest],
    /// unmodified.
    pub state: State,
    /// The issuer identifier for client validation.
    ///
    /// Clients MUST verify this matches the [`Metadata::issuer`]
    /// parameter provided by the Server [Metadata] endpoint during
    /// Discovery as outlined in [OAuth 2.0 Authorization Server
    /// Issuer Identification][oauth2-iss]. If the value does not
    /// match the expected issuer identifier, clients MUST reject the
    /// authorization response and MUST NOT proceed with the
    /// authorization grant. For error responses, clients MUST NOT
    /// assume that the error originates from the intended
    /// authorization server.
    ///
    /// [oauth2-iss]: https://www.ietf.org/archive/id/draft-ietf-oauth-iss-auth-resp-02.html
    pub iss: Url
}

/// A grant request that continues the IndieAuth ceremony.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "grant_type")]
#[serde(rename_all = "snake_case")]
pub enum GrantRequest {
    /// Use an authorization code to receive identity verification
    /// and/or an access token.
    AuthorizationCode {
        /// The code from [`AuthorizationResponse`].
        code: String,
        /// Client ID that this grant belongs to.
        client_id: Url,
        /// Redirect URI that was used to receive the grant.
        redirect_uri: Url,
        /// The PKCE code verifier that was used to create the code
        /// challenge.
        code_verifier: PKCEVerifier
    },
    /// Use a refresh token to get a fresh access token and a new
    /// matching refresh token.
    RefreshToken {
        /// The refresh token that was issued before.
        refresh_token: String,
        /// The client ID to which the token belongs to.
        client_id: url::Url,
        /// A list of scopes, not exceeding the already-granted scope,
        /// that can be passed to further restrict the scopes on the
        /// new token.
        ///
        /// This cannot be used to gain new scopes -- you need to
        /// start over if you need new scopes from the user.
        scope: Option<Scopes>
    }
}

/// Token type, as described in [RFC6749][].
///
/// [RFC6749]: https://www.rfc-editor.org/rfc/rfc6749#section-7.1
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenType {
    /// A Bearer token described in [RFC6750][]. As far as the author
    /// of this library is concerned, this is the only type that
    /// IndieAuth uses.
    ///
    /// [RFC6750]: https://www.rfc-editor.org/rfc/rfc6750
    Bearer
}

/// The response to a successful [`GrantRequest`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum GrantResponse {
    /// An access token response, containing an access token, a refresh
    /// token (if the identity provider supports them) and the profile
    /// (if access was granted to the profile data).
    AccessToken {
        /// The URL for the user this token corresponds to.
        me: Url,
        /// Token type. Required by OAuth2, not mentioned in
        /// IndieAuth. Confirmed as erroneous.
        token_type: TokenType,
        /// Scopes. REQUIRED if different from what was
        /// requested. Absence from IndieAuth spec confirmed as
        /// erroneous.
        scope: Option<Scopes>,
        /// The user's profile information, if it was requested.
        #[serde(skip_serializing_if = "Option::is_none")]
        profile: Option<Profile>,
        /// The access token that can be used to access protected resources.
        access_token: String,
        /// The duration in which the access token expires, represented in seconds.
        // TODO replace with std::time::Duration
        #[serde(skip_serializing_if = "Option::is_none")]
        expires_in: Option<u64>,
        /// The refresh token, if it was issued.
        #[serde(skip_serializing_if = "Option::is_none")]
        refresh_token: Option<String>
    },
    /// A profile URL response, that only contains the profile URL and
    /// the profile, if it was requested.
    ///
    /// This is suitable for confirming the identity of the user, but
    /// no more than that.
    ProfileUrl {
        /// The authenticated user's URL.
        me: Url,
        /// The user's profile information, if it was requested.
        #[serde(skip_serializing_if = "Option::is_none")]
        profile: Option<Profile>
    }
}

#[cfg(feature = "axum")]
impl axum_core::response::IntoResponse for GrantResponse {
    fn into_response(self) -> axum_core::response::Response {
        use http::StatusCode;

        (StatusCode::OK,
         [("Content-Type", "application/json"),
          ("Cache-Control", "no-store"),
          ("Pragma", "no-cache")
         ],
         serde_json::to_vec(&self).unwrap())
            .into_response()
    }
}

/// Describes requests that the authorization endpoint might want to handle.
///
/// This type mostly exists for ease-of-use with serde.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[allow(missing_docs)]
pub enum RequestMaybeAuthorizationEndpoint {
    Authorization(AuthorizationRequest),
    Grant(GrantRequest)
}

/// A token introspection request that can be handled by the token
/// introspection endpoint.
///
/// Note that this request doesn't contain authentication data, which
/// is commonly transmitted out-of-band (e.g. via the `Authorization`
/// header).
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenIntrospectionRequest {
    /// The token for which data was requested.
    pub token: String
}

/// Data for a token that will be returned by the introspection
/// endpoint (and can also be used internally by the resource server
/// if it is part of a monolith with the identity provider).
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenData {
    /// The user this token corresponds to.
    pub me: Url,
    /// The client ID for the client that this token was issued to.
    pub client_id: Url,
    /// Scope that was granted to this token.
    pub scope: Scopes,
    /// The expiration date for this token, measured in seconds from
    /// the Unix time epoch (1970-01-01 00:00:00).
    // TODO replace these two with std::time::SystemTime
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exp: Option<u64>,
    /// The issue date, represented in the same format as the
    /// [`exp`][TokenData::exp] field.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iat: Option<u64>
}

impl TokenData {
    /// Check if the token in question expired.
    pub fn expired(&self) -> bool {
        use std::time::{Duration, SystemTime, UNIX_EPOCH};
        
        self.exp
            .map(|exp| SystemTime::now()
                 .duration_since(UNIX_EPOCH)
                 .unwrap_or(Duration::ZERO)
                 .as_secs() >= exp)
            .unwrap_or_default()
    }

    /// Return a timestamp at which the token is not considered valid anymore.
    pub fn expires_at(&self) -> Option<std::time::SystemTime> {
        self.exp.map(|time| {
            std::time::UNIX_EPOCH + std::time::Duration::from_secs(time)
        })
    }
    /// Return a timestamp describing when the token was issued.
    pub fn issued_at(&self) -> Option<std::time::SystemTime> {
        self.iat.map(|time| {
            std::time::UNIX_EPOCH + std::time::Duration::from_secs(time)
        })
    }

    /// Check if a certain scope is allowed for this token.
    pub fn check_scope(&self, scope: &Scope) -> bool {
        self.scope.has(scope)
    }
}

// I don't like this type, because it could've been represented
// internally by Option<TokenData>. But the IndieAuth standard
// requires the "active" field to be present. I can't do anything
// about it.
/// The introspection response that the introspection endpoint must
/// return.
///
/// It is recommended to use the [`From`][`std::convert::From`] trait
/// to convert from `Option<TokenData>` for ergonomics.
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenIntrospectionResponse {
    active: bool,
    #[serde(flatten)]
    #[serde(skip_serializing_if = "Option::is_none")]
    data: Option<TokenData>
}
// These wrappers and impls should take care of making use of this
// type as painless as possible.
impl TokenIntrospectionResponse {
    /// Indicate that this token is not valid.
    pub fn inactive() -> Self {
        Self { active: false, data: None }
    }
    /// Indicate that this token is valid, and provide data about it.
    pub fn active(data: TokenData) -> Self {
        Self { active: true, data: Some(data) }
    }
    /// Check if the endpoint reports this token as valid.
    pub fn is_active(&self) -> bool {
        self.active
    }

    /// Get data contained in the response, if the token is valid.
    pub fn data(&self) -> Option<&TokenData> {
        if !self.active {
            return None
        }
        self.data.as_ref()
    }
}
impl Default for TokenIntrospectionResponse {
    fn default() -> Self {
        Self::inactive()
    }
}
impl From<Option<TokenData>> for TokenIntrospectionResponse {
    fn from(data: Option<TokenData>) -> Self {
        Self { active: data.is_some(), data }
    }
}
impl From<TokenIntrospectionResponse> for Option<TokenData> {
    fn from(response: TokenIntrospectionResponse) -> Option<TokenData> {
        response.data
    }
}

#[cfg(feature = "axum")]
impl axum_core::response::IntoResponse for TokenIntrospectionResponse {
    fn into_response(self) -> axum_core::response::Response {
        use http::StatusCode;

        (StatusCode::OK,
         [("Content-Type", "application/json")],
         serde_json::to_vec(&self).unwrap())
            .into_response()
    }
}

/// A request for revoking a token. There is no response beyond `HTTP
/// 200 OK`.
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenRevocationRequest {
    /// The token that needs to be revoked in case it is valid.
    pub token: String
}

/// Types of errors that a resource server (IndieAuth consumer) can
/// throw when authentication goes wrong.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResourceErrorKind {
    /// The provided token was invalid.
    InvalidToken,
    /// The scope on the token was insufficient to perform the
    /// requested operation.
    InsufficientScope,
}

/// Various kinds of errors that could occur when performing the
/// IndieAuth ceremony.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorKind {
    /// The request is missing a required parameter, includes an
    /// unsupported parameter value (other than grant type), repeats a
    /// parameter, includes multiple credentials, utilizes more than
    /// one mechanism for authenticating the client, or is otherwise
    /// malformed.
    InvalidRequest,
    /// Client authentication failed (e.g., unknown client, no client
    /// authentication included, or unsupported authentication
    /// method).  The authorization server MAY return an HTTP 401
    /// (Unauthorized) status code to indicate which HTTP
    /// authentication schemes are supported.  If the client attempted
    /// to authenticate via the "Authorization" request header field,
    /// the authorization server MUST respond with an HTTP 401
    /// (Unauthorized) status code and include the "WWW-Authenticate"
    /// response header field matching the authentication scheme used
    /// by the client.
    InvalidClient,
    /// The provided authorization grant (e.g., authorization
    /// code, resource owner credentials) or refresh token is
    /// invalid, expired, revoked, does not match the redirection
    /// URI used in the authorization request, or was issued to
    /// another client.
    InvalidGrant,
    /// The authenticated client is not authorized to use this
    /// authorization grant type.
    UnauthorizedClient,
    /// The authorization grant type is not supported by the
    /// authorization server.
    UnsupportedGrantType,
    /// The requested scope is invalid, unknown, malformed, or
    /// exceeds the scope granted by the resource owner.
    InvalidScope
}
// TODO consider relying on serde_variant for these conversions
impl AsRef<str> for ErrorKind {
    fn as_ref(&self) -> &str {
        match self {
            ErrorKind::InvalidRequest => "invalid_request",
            ErrorKind::InvalidClient => "invalid_client",
            ErrorKind::InvalidGrant => "invalid_grant",
            ErrorKind::UnauthorizedClient => "unauthorized_client",
            ErrorKind::UnsupportedGrantType => "unsupported_grant_type",
            ErrorKind::InvalidScope => "invalid_scope",
        }
    }
}
impl std::fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_ref())
    }
}

/// An error that can be returned when performing the IndieAuth ceremony.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Error {
    /// Type of an error.
    #[serde(rename = "error")]
    pub kind: ErrorKind,
    /// Human-friendly description of an error, suitable for a
    /// developer to read while debugging.
    #[serde(rename = "error_description")]
    pub msg: Option<String>,
    /// An URL to documentation describing what went wrong and how to
    /// fix it.
    pub error_uri: Option<url::Url>
}

impl From<ErrorKind> for Error {
    fn from(kind: ErrorKind) -> Error {
        Error {
            kind, msg: None, error_uri: None
        }
    }
}

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

impl std::fmt::Display for self::Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "IndieAuth error ({})", self.kind)?;
        if let Some(msg) = self.msg.as_deref() {
            write!(f, ": {}", msg)?;
        }
        if let Some(error_uri) = &self.error_uri {
            write!(f, " (see `{}` for more info)", error_uri)?;
        }

        Ok(())
    }
}

#[cfg(feature = "axum")]
impl axum_core::response::IntoResponse for self::Error {
    fn into_response(self) -> axum_core::response::Response {
        use http::StatusCode;

        (StatusCode::BAD_REQUEST,
         [("Content-Type", "application/json")],
         serde_json::to_vec(&self).unwrap())
            .into_response()
    }
}

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

    #[test]
    fn test_serialize_deserialize_grant_request() {
        let authorization_code: GrantRequest = GrantRequest::AuthorizationCode {
            client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
            redirect_uri: "https://kittybox.fireburn.ru/.kittybox/login/redirect".parse().unwrap(),
            code_verifier: PKCEVerifier("helloworld".to_string()),
            code: "hithere".to_owned()
        };
        let serialized = serde_urlencoded::to_string(&[
            ("grant_type", "authorization_code"),
            ("code", "hithere"),
            ("client_id", "https://kittybox.fireburn.ru/"),
            ("redirect_uri", "https://kittybox.fireburn.ru/.kittybox/login/redirect"),
            ("code_verifier", "helloworld"),
        ]).unwrap();

        let deserialized = serde_urlencoded::from_str(&serialized).unwrap();

        assert_eq!(authorization_code, deserialized);

        assert_eq!(
            serialized,
            serde_urlencoded::to_string(authorization_code).unwrap()
        )
    }
}