use crate::title::Title;
use serde_json::Value;
use std::error::Error;
use std::fmt;

/// Error type for all MediaWiki API operations.
#[derive(Debug)]
#[non_exhaustive]
pub enum MediaWikiError {
    /// JSON serialisation/deserialisation error.
    Serde(serde_json::Error),
    /// HTTP client error from `reqwest`.
    Reqwest(reqwest::Error),
    /// Invalid HTTP header value.
    ReqwestHeader(reqwest::header::InvalidHeaderValue),
    /// Generic string error.
    String(String),
    /// URL parse error.
    Url(url::ParseError),
    /// Formatting error.
    Fmt(fmt::Error),
    /// System time error.
    Time(std::time::SystemTimeError),

    /// Error while logging in.
    Login(String),

    // These are errors for the Page struct
    /// Couldn't obtain the title for this page for use in an API request.
    BadTitle(Title),

    /// Couldn't understand the API response (provided).
    BadResponse(Value),

    /// Missing page.
    Missing(Title),

    /// Edit failed; API response is provided.
    EditError(Value),

    /// Unexpected data structure (eg array instead of object) in API JSON result
    UnexpectedResultFormat(String),
}

impl Error for MediaWikiError {}

impl fmt::Display for MediaWikiError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Serde(e) => f.write_str(&e.to_string()),
            Self::Reqwest(e) => f.write_str(&e.to_string()),
            Self::ReqwestHeader(e) => f.write_str(&e.to_string()),
            Self::String(s) => f.write_str(s),
            Self::Url(e) => f.write_str(&e.to_string()),
            Self::Fmt(e) => f.write_str(&e.to_string()),
            Self::Time(e) => f.write_str(&e.to_string()),
            Self::Login(s) => f.write_str(s),

            Self::BadTitle(title) => write!(f, "invalid title for this Page: {:?}", title),
            Self::BadResponse(response) => write!(
                f,
                "bad API response while fetching revision content: {:?}",
                response
            ),
            Self::Missing(title) => write!(f, "page missing: {:?}", title),
            Self::EditError(response) => write!(f, "edit resulted in error: {:?}", response),
            Self::UnexpectedResultFormat(error) => write!(f, "result format error: {}", error),
        }
    }
}

impl From<serde_json::Error> for MediaWikiError {
    fn from(e: serde_json::Error) -> Self {
        Self::Serde(e)
    }
}

impl From<reqwest::Error> for MediaWikiError {
    fn from(e: reqwest::Error) -> Self {
        Self::Reqwest(e)
    }
}

impl From<reqwest::header::InvalidHeaderValue> for MediaWikiError {
    fn from(e: reqwest::header::InvalidHeaderValue) -> Self {
        Self::ReqwestHeader(e)
    }
}

impl From<reqwest::header::ToStrError> for MediaWikiError {
    fn from(e: reqwest::header::ToStrError) -> Self {
        Self::String(e.to_string())
    }
}

impl From<String> for MediaWikiError {
    fn from(e: String) -> Self {
        Self::String(e)
    }
}

impl From<&str> for MediaWikiError {
    fn from(e: &str) -> Self {
        Self::String(e.to_string())
    }
}

impl From<url::ParseError> for MediaWikiError {
    fn from(e: url::ParseError) -> Self {
        Self::Url(e)
    }
}

impl From<fmt::Error> for MediaWikiError {
    fn from(e: fmt::Error) -> Self {
        Self::Fmt(e)
    }
}

impl From<std::time::SystemTimeError> for MediaWikiError {
    fn from(e: std::time::SystemTimeError) -> Self {
        Self::Time(e)
    }
}

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

    #[test]
    fn from_string() {
        let err = MediaWikiError::from("test error".to_string());
        assert!(matches!(err, MediaWikiError::String(s) if s == "test error"));
    }

    #[test]
    fn from_str() {
        let err = MediaWikiError::from("test error");
        assert!(matches!(err, MediaWikiError::String(s) if s == "test error"));
    }

    #[test]
    fn display_string_error() {
        let err = MediaWikiError::String("something went wrong".to_string());
        assert_eq!(format!("{}", err), "something went wrong");
    }

    #[test]
    fn display_missing() {
        let title = Title::new("Test Page", 0);
        let err = MediaWikiError::Missing(title);
        let msg = format!("{}", err);
        assert!(msg.contains("missing"));
    }

    #[test]
    fn display_login() {
        let err = MediaWikiError::Login("bad credentials".to_string());
        assert_eq!(format!("{}", err), "bad credentials");
    }

    #[test]
    fn from_url_parse_error() {
        let url_err = url::Url::parse("not a url :::").unwrap_err();
        let err = MediaWikiError::from(url_err);
        assert!(matches!(err, MediaWikiError::Url(_)));
    }

    #[test]
    fn from_serde_error() {
        let serde_err = serde_json::from_str::<Value>("not json{{{").unwrap_err();
        let err = MediaWikiError::from(serde_err);
        assert!(matches!(err, MediaWikiError::Serde(_)));
    }

    #[test]
    fn error_trait_implemented() {
        let err = MediaWikiError::String("test".to_string());
        // Verify it implements std::error::Error
        let _: &dyn Error = &err;
    }
}
