//! Unified error type for `dnscrypto`.

use std::fmt;

/// Errors that can occur during a `DNSCrypt` session.
#[derive(Debug)]
pub enum Error {
    /// A network-level error occurred (UDP/TCP send, receive, or connect).
    Network(String),
    /// The `DNSCrypt` resolver certificate was missing, invalid, or expired.
    CertificateError(String),
    /// Poly1305 authentication of an encrypted DNS packet failed.
    ///
    /// This indicates tampered or corrupted data and should be treated as a
    /// security event.
    AuthenticationFailure,
    /// The decrypted DNS response contained no A record for the queried name.
    NoRecord,
    /// A protocol-level invariant was violated (e.g. wrong magic bytes,
    /// nonce mismatch, or malformed packet structure).
    Protocol(String),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Network(msg) => write!(f, "DNSCrypt network error: {msg}"),
            Self::CertificateError(msg) => write!(f, "DNSCrypt certificate error: {msg}"),
            Self::AuthenticationFailure => {
                write!(f, "DNSCrypt authentication failure (Poly1305 tag mismatch)")
            }
            Self::NoRecord => write!(f, "DNSCrypt: no A record found in response"),
            Self::Protocol(msg) => write!(f, "DNSCrypt protocol error: {msg}"),
        }
    }
}

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

/// Convert a bare `String` error (from internal helpers) into the most
/// appropriate [`Error`] variant.  Internal helpers still use `String` for
/// ergonomics; this conversion happens at the public API boundary.
impl From<String> for Error {
    fn from(s: String) -> Self {
        // Classify by message prefix emitted by sub-modules.
        if s.contains("authentication") || s.contains("Poly1305") {
            Self::AuthenticationFailure
        } else if s.contains("TCP") || s.contains("UDP") || s.contains("socket") {
            Self::Network(s)
        } else if s.contains("cert") || s.contains("Cert") || s.contains("serial") {
            Self::CertificateError(s)
        } else {
            Self::Protocol(s)
        }
    }
}

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

    #[test]
    fn test_display_messages() {
        assert_eq!(
            Error::Network("timed out".to_string()).to_string(),
            "DNSCrypt network error: timed out"
        );
        assert_eq!(
            Error::CertificateError("expired".to_string()).to_string(),
            "DNSCrypt certificate error: expired"
        );
        assert_eq!(
            Error::AuthenticationFailure.to_string(),
            "DNSCrypt authentication failure (Poly1305 tag mismatch)"
        );
        assert_eq!(
            Error::NoRecord.to_string(),
            "DNSCrypt: no A record found in response"
        );
        assert_eq!(
            Error::Protocol("bad magic".to_string()).to_string(),
            "DNSCrypt protocol error: bad magic"
        );
    }

    #[test]
    fn test_from_string_classification() {
        // Each disjunct below is exercised both as the deciding (first
        // matching) clause and, where it isn't the first alternative, with
        // the earlier alternative(s) absent so short-circuit evaluation
        // still reaches it.
        assert!(matches!(
            Error::from("authentication failed".to_string()),
            Error::AuthenticationFailure
        ));
        assert!(matches!(
            Error::from("Poly1305 tag mismatch".to_string()),
            Error::AuthenticationFailure
        ));
        assert!(matches!(
            Error::from("TCP connect failed: oops".to_string()),
            Error::Network(_)
        ));
        assert!(matches!(
            Error::from("UDP send failed: oops".to_string()),
            Error::Network(_)
        ));
        assert!(matches!(
            Error::from("failed to bind socket".to_string()),
            Error::Network(_)
        ));
        assert!(matches!(
            Error::from("cert serial 1 < cached 2".to_string()),
            Error::CertificateError(_)
        ));
        assert!(matches!(
            Error::from("Certificate expired".to_string()),
            Error::CertificateError(_)
        ));
        assert!(matches!(
            Error::from("duplicate serial number".to_string()),
            Error::CertificateError(_)
        ));
        assert!(matches!(
            Error::from("something else entirely".to_string()),
            Error::Protocol(_)
        ));
    }
}
