// Copyright (c) 2022-2023 Yuki Kishimoto
// Copyright (c) 2023-2025 Rust Nostr Developers
// Distributed under the MIT software license

//! NIP-98: HTTP Auth
//!
//! This NIP defines an ephemeral event used to authorize requests to HTTP servers using nostr events.
//! This is useful for HTTP services which are build for Nostr and deal with Nostr user accounts.
//!
//! <https://github.com/nostr-protocol/nips/blob/master/98.md>

use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use core::str::FromStr;

#[cfg(feature = "std")]
use base64::engine::{Engine, general_purpose};
#[cfg(feature = "std")]
use hashes::Hash;
use hashes::hex::HexToArrayError;
use hashes::sha256::Hash as Sha256Hash;

use super::util::take_and_parse_from_str;
use crate::Url;
#[cfg(all(feature = "std", feature = "rand"))]
use crate::event::EventBuilder;
#[cfg(all(feature = "std", feature = "rand"))]
use crate::event::FinalizeEventAsync;
use crate::event::tag::{Tag, TagCodec, TagCodecError, impl_tag_codec_conversions};
#[cfg(feature = "std")]
use crate::event::{self, Event};
#[cfg(feature = "std")]
use crate::signer::SignerError;
#[cfg(all(feature = "std", feature = "rand"))]
use crate::signer::{AsyncGetPublicKey, AsyncSignEvent};
use crate::types::url;
#[cfg(feature = "std")]
use crate::util::JsonUtil;
#[cfg(feature = "std")]
use crate::{Kind, PublicKey, Timestamp};

#[cfg(feature = "std")]
const AUTH_HEADER_PREFIX: &str = "Nostr";
const ABSOLUTE_URL: &str = "u";
const METHOD: &str = "method";
const PAYLOAD: &str = "payload";

/// [`HttpData`] required tags
#[derive(Debug, PartialEq, Eq)]
pub enum RequiredTags {
    /// `u`
    AbsoluteURL,
    /// `method`
    Method,
    /// `payload`
    Payload,
}

impl fmt::Display for RequiredTags {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::AbsoluteURL => f.write_str("u"),
            Self::Method => f.write_str("method"),
            Self::Payload => f.write_str("payload"),
        }
    }
}

/// NIP98 error
#[derive(Debug, PartialEq)]
pub enum Error {
    /// Base64 error
    #[cfg(feature = "std")]
    Base64(base64::DecodeError),
    /// Event error
    #[cfg(feature = "std")]
    Event(event::Error),
    /// Event builder error
    #[cfg(feature = "std")]
    Signer(SignerError),
    /// URL parse error
    Url(url::ParseError),
    /// Hex decoding error
    Hex(HexToArrayError),
    /// Codec error
    Codec(TagCodecError),
    /// Tag missing when parsing
    MissingTag(RequiredTags),
    /// Invalid HTTP Method
    UnknownMethod,
    /// Nostr authorization header missing
    #[cfg(feature = "std")]
    AuthorizationHeaderMissing,
    /// Malformed authorization header
    #[cfg(feature = "std")]
    MalformedAuthorizationHeader,
    /// Unexpected authorization header kind
    #[cfg(feature = "std")]
    WrongAuthHeaderKind,
    /// Authorization doesn't match request
    #[cfg(feature = "std")]
    AuthorizationNotMatchRequest {
        /// The authorized url
        authorized_url: Box<Url>,
        /// The authorized url
        authorized_method: HttpMethod,
        /// The request url
        request_url: Box<Url>,
        /// The request url
        request_method: HttpMethod,
    },
    /// Authorization is too old
    #[cfg(feature = "std")]
    AuthorizationTooOld {
        /// Current timestamp
        current: Timestamp,
        /// Auth event created at
        created_at: Timestamp,
    },
    /// Payload hash doesn't match the body hash
    #[cfg(feature = "std")]
    PayloadHashMismatch,
}

impl core::error::Error for Error {}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            #[cfg(feature = "std")]
            Self::Base64(e) => e.fmt(f),
            #[cfg(feature = "std")]
            Self::Event(e) => e.fmt(f),
            #[cfg(feature = "std")]
            Self::Signer(e) => e.fmt(f),
            Self::Url(e) => e.fmt(f),
            Self::Hex(e) => e.fmt(f),
            Self::Codec(e) => e.fmt(f),
            Self::MissingTag(tag) => write!(f, "missing '{tag}' tag"),
            Self::UnknownMethod => f.write_str("Unknown HTTP method"),
            #[cfg(feature = "std")]
            Self::AuthorizationHeaderMissing => f.write_str("nostr authorization header missing"),
            #[cfg(feature = "std")]
            Self::MalformedAuthorizationHeader => {
                f.write_str("malformed nostr authorization header")
            }
            #[cfg(feature = "std")]
            Self::WrongAuthHeaderKind => f.write_str("wrong nostr authorization header kind"),
            #[cfg(feature = "std")]
            Self::AuthorizationNotMatchRequest {
                authorized_url,
                authorized_method,
                request_url,
                request_method,
            } => write!(
                f,
                "authorization doesn't match request: authorized_url={authorized_url}, authorized_method={authorized_method}, request_url={request_url}, request_method={request_method}"
            ),
            #[cfg(feature = "std")]
            Self::AuthorizationTooOld {
                current,
                created_at,
            } => write!(
                f,
                "authorization event is too old: current_time={current}, created_at={created_at}"
            ),
            #[cfg(feature = "std")]
            Self::PayloadHashMismatch => f.write_str("payload hash doesn't match the body hash"),
        }
    }
}

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

#[cfg(feature = "std")]
impl From<base64::DecodeError> for Error {
    fn from(e: base64::DecodeError) -> Self {
        Self::Base64(e)
    }
}

#[cfg(feature = "std")]
impl From<event::Error> for Error {
    fn from(e: event::Error) -> Self {
        Self::Event(e)
    }
}

#[cfg(feature = "std")]
impl From<SignerError> for Error {
    fn from(e: SignerError) -> Self {
        Self::Signer(e)
    }
}

impl From<HexToArrayError> for Error {
    fn from(e: HexToArrayError) -> Self {
        Self::Hex(e)
    }
}

impl From<TagCodecError> for Error {
    fn from(e: TagCodecError) -> Self {
        Self::Codec(e)
    }
}

/// HTTP Method
///
/// <https://github.com/nostr-protocol/nips/blob/master/98.md>
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum HttpMethod {
    /// GET
    GET,
    /// POST
    POST,
    /// PUT
    PUT,
    /// PATCH
    PATCH,
}

impl fmt::Display for HttpMethod {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl HttpMethod {
    /// Get as `&str`
    pub fn as_str(&self) -> &str {
        match self {
            Self::GET => "GET",
            Self::POST => "POST",
            Self::PUT => "PUT",
            Self::PATCH => "PATCH",
        }
    }
}

impl FromStr for HttpMethod {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "GET" => Ok(Self::GET),
            "POST" => Ok(Self::POST),
            "PUT" => Ok(Self::PUT),
            "PATCH" => Ok(Self::PATCH),
            _ => Err(Error::UnknownMethod),
        }
    }
}

/// HTTP Data
///
/// <https://github.com/nostr-protocol/nips/blob/master/98.md>
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HttpData {
    /// Absolute request URL
    pub url: Url,
    /// HTTP method
    pub method: HttpMethod,
    /// SHA256 hash of the request body
    pub payload: Option<Sha256Hash>,
}

/// NIP-98 tags
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Nip98Tag {
    /// `u` tag
    AbsoluteURL(Url),
    /// `method` tag
    Method(HttpMethod),
    /// `payload` tag
    Payload(Sha256Hash),
}

impl TagCodec for Nip98Tag {
    type Error = Error;

    fn parse<I, S>(tag: I) -> Result<Self, Self::Error>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<str>,
    {
        let mut iter = tag.into_iter();
        let kind: S = iter.next().ok_or(TagCodecError::missing_tag_kind())?;

        match kind.as_ref() {
            ABSOLUTE_URL => {
                let url: Url =
                    take_and_parse_from_str::<_, _, _, Error>(&mut iter, "absolute URL")?;
                Ok(Self::AbsoluteURL(url))
            }
            METHOD => {
                let method: HttpMethod =
                    take_and_parse_from_str::<_, _, _, Error>(&mut iter, "method")?;
                Ok(Self::Method(method))
            }
            PAYLOAD => {
                let payload: Sha256Hash =
                    take_and_parse_from_str::<_, _, _, Error>(&mut iter, "payload")?;
                Ok(Self::Payload(payload))
            }
            _ => Err(TagCodecError::Unknown.into()),
        }
    }

    fn to_tag(&self) -> Tag {
        match self {
            Self::AbsoluteURL(url) => Tag::new(vec![String::from(ABSOLUTE_URL), url.to_string()]),
            Self::Method(method) => Tag::new(vec![String::from(METHOD), method.to_string()]),
            Self::Payload(payload) => Tag::new(vec![String::from(PAYLOAD), payload.to_string()]),
        }
    }
}

impl_tag_codec_conversions!(Nip98Tag);

impl HttpData {
    /// New [`HttpData`]
    #[inline]
    pub fn new(url: Url, method: HttpMethod) -> Self {
        Self {
            url,
            method,
            payload: None,
        }
    }

    /// Add hex-encoded SHA256 hash of the request body
    #[inline]
    pub fn payload(mut self, payload: Sha256Hash) -> Self {
        self.payload = Some(payload);
        self
    }

    /// Build the base64-encoded HTTP `Authorization` header **value**.
    ///
    /// Return a string with the following format: `Nostr <base64>`.
    #[cfg(all(feature = "std", feature = "rand"))]
    pub async fn to_authorization<T>(self, signer: &T) -> Result<String, Error>
    where
        T: AsyncGetPublicKey + AsyncSignEvent,
    {
        let event: Event = EventBuilder::http_auth(self).finalize_async(signer).await?;
        let encoded: String = general_purpose::STANDARD.encode(event.as_json());
        Ok(format!("{AUTH_HEADER_PREFIX} {encoded}"))
    }
}

impl From<HttpData> for Vec<Tag> {
    fn from(data: HttpData) -> Self {
        let HttpData {
            url,
            method,
            payload,
        } = data;

        let mut tags: Vec<Tag> = vec![
            Nip98Tag::AbsoluteURL(url).to_tag(),
            Nip98Tag::Method(method).to_tag(),
        ];
        if let Some(payload) = payload {
            tags.push(Nip98Tag::Payload(payload).to_tag());
        }

        tags
    }
}

impl TryFrom<Vec<Tag>> for HttpData {
    type Error = Error;

    fn try_from(value: Vec<Tag>) -> Result<Self, Self::Error> {
        let mut url: Option<Url> = None;
        let mut method: Option<HttpMethod> = None;
        let mut payload: Option<Sha256Hash> = None;

        for tag in value.into_iter() {
            match Nip98Tag::try_from(tag) {
                Ok(Nip98Tag::AbsoluteURL(value)) => url = Some(value),
                Ok(Nip98Tag::Method(value)) => method = Some(value),
                Ok(Nip98Tag::Payload(value)) => payload = Some(value),
                Err(_) => (),
            }
        }

        Ok(Self {
            url: url.ok_or(Error::MissingTag(RequiredTags::AbsoluteURL))?,
            method: method.ok_or(Error::MissingTag(RequiredTags::Method))?,
            payload,
        })
    }
}

/// Verify authentication header
///
/// Returns the event [`PublicKey`] if the authorization is valid.
///
/// This functions execute the following checks:
/// - Extract the prefix and the base64 event from the header;
/// - Decode the base64 event and check if the kind is correct;
/// - Check if the tags are right;
/// - Check if the auth event is too old
/// - If there is a body, verify if the payload hash matches the body hash;
/// - Verify the event ID and signature (to learn more check [`Event::verify`]).
///
/// <https://github.com/nostr-protocol/nips/blob/master/98.md>
#[cfg(feature = "std")]
pub fn verify_auth_header(
    auth_header: &str,
    url: &Url,
    method: HttpMethod,
    current_time: Timestamp,
    body: Option<&[u8]>,
) -> Result<PublicKey, Error> {
    // Original code at https://github.com/damus-io/notepush/blob/63c5f7e7236f7bfe09f665b5fb4a03b412284d13/src/nip98_auth.rs

    if auth_header.is_empty() {
        return Err(Error::AuthorizationHeaderMissing);
    }

    let (prefix, base64_encoded_event): (&str, &str) = auth_header
        .split_once(' ')
        .ok_or(Error::MalformedAuthorizationHeader)?;

    if prefix != AUTH_HEADER_PREFIX || base64_encoded_event.is_empty() {
        return Err(Error::MalformedAuthorizationHeader);
    }

    // Decode event
    let decoded_event_json: Vec<u8> = general_purpose::STANDARD.decode(base64_encoded_event)?;
    let event: Event = Event::from_json(decoded_event_json)?;

    // Check event kind
    if event.kind != Kind::HttpAuth {
        return Err(Error::WrongAuthHeaderKind);
    }

    let http_data = HttpData::try_from(event.tags.iter().cloned().collect::<Vec<Tag>>())?;
    let authorized_url: Url = http_data.url;
    let authorized_method: HttpMethod = http_data.method;

    if &authorized_url != url || authorized_method != method {
        return Err(Error::AuthorizationNotMatchRequest {
            authorized_url: Box::new(authorized_url.clone()),
            authorized_method,
            request_url: Box::new(url.clone()),
            request_method: method,
        });
    }

    let time_delta = TimeDelta::subtracting(current_time, event.created_at);
    if (time_delta.negative && time_delta.delta_abs_seconds > 30)
        || (!time_delta.negative && time_delta.delta_abs_seconds > 60)
    {
        return Err(Error::AuthorizationTooOld {
            current: current_time,
            created_at: event.created_at,
        });
    }

    if let Some(body_data) = body {
        // Get payload hash
        let payload: Sha256Hash = match http_data.payload {
            Some(p) => p,
            None => return Err(Error::MissingTag(RequiredTags::Payload)),
        };

        // Hash body data
        let body_hash: Sha256Hash = Sha256Hash::hash(body_data);

        // Check if payload and body hash matches
        if payload != body_hash {
            return Err(Error::PayloadHashMismatch);
        }
    }

    // Verify both the Event ID and the cryptographic signature
    event.verify()?;

    Ok(event.pubkey)
}

#[cfg(feature = "std")]
struct TimeDelta {
    pub delta_abs_seconds: u64,
    pub negative: bool,
}

#[cfg(feature = "std")]
impl TimeDelta {
    /// Safely calculate the difference between two timestamps in seconds
    /// This function is safer against overflows than subtracting the timestamps directly
    pub fn subtracting(t1: Timestamp, t2: Timestamp) -> TimeDelta {
        if t1 > t2 {
            TimeDelta {
                delta_abs_seconds: (t1 - t2).as_secs(),
                negative: false,
            }
        } else {
            TimeDelta {
                delta_abs_seconds: (t2 - t1).as_secs(),
                negative: true,
            }
        }
    }
}

#[cfg(all(test, feature = "std"))]
mod tests {
    use super::*;

    #[test]
    fn test_nip98_tag_codec() {
        let url = Nip98Tag::parse(["u", "https://example.com/"]).unwrap();
        assert_eq!(
            url,
            Nip98Tag::AbsoluteURL(Url::parse("https://example.com/").unwrap())
        );
        assert_eq!(
            url.to_tag(),
            Tag::parse(["u", "https://example.com/"]).unwrap()
        );

        let method = Nip98Tag::parse(["method", "GET"]).unwrap();
        assert_eq!(method, Nip98Tag::Method(HttpMethod::GET));
        assert_eq!(method.to_tag(), Tag::parse(["method", "GET"]).unwrap());

        let payload_hash = Sha256Hash::from_str(
            "12f8ff0f5f6f023a4ae796a5f5f6d9030434bf2b9bb7a2f4f0f0f971b3e5d79f",
        )
        .unwrap();
        let payload = Nip98Tag::parse([
            "payload",
            "12f8ff0f5f6f023a4ae796a5f5f6d9030434bf2b9bb7a2f4f0f0f971b3e5d79f",
        ])
        .unwrap();
        assert_eq!(payload, Nip98Tag::Payload(payload_hash));
    }

    #[test]
    fn test_nip98_http_data_round_trip() {
        let payload = Sha256Hash::from_str(
            "12f8ff0f5f6f023a4ae796a5f5f6d9030434bf2b9bb7a2f4f0f0f971b3e5d79f",
        )
        .unwrap();
        let data = HttpData::new(
            Url::parse("https://example.com/").unwrap(),
            HttpMethod::POST,
        )
        .payload(payload);

        let tags: Vec<Tag> = data.clone().into();
        let parsed = HttpData::try_from(tags).unwrap();

        assert_eq!(parsed, data);
    }

    #[test]
    fn empty_auth_header() {
        let url = Url::parse("https://example.com/").unwrap();
        assert_eq!(
            verify_auth_header("", &url, HttpMethod::GET, Timestamp::now(), None).unwrap_err(),
            Error::AuthorizationHeaderMissing
        );
    }

    #[test]
    fn malformed_auth_header() {
        let url = Url::parse("https://example.com/").unwrap();
        let now = Timestamp::now();
        assert_eq!(
            verify_auth_header("Test Nostr", &url, HttpMethod::GET, now, None).unwrap_err(),
            Error::MalformedAuthorizationHeader
        );
        assert_eq!(
            verify_auth_header("Nostr", &url, HttpMethod::GET, now, None).unwrap_err(),
            Error::MalformedAuthorizationHeader
        );
        assert_eq!(verify_auth_header("nostr eyJpZCI6ImZlOTY0ZTc1ODkwMzM2MGYyOGQ4NDI0ZDA5MmRhODQ5NGVkMjA3Y2JhODIzMTEwYmUzYTU3ZGZlNGI1Nzg3MzQiLCJwdWJrZXkiOiI2M2ZlNjMxOGRjNTg1ODNjZmUxNjgxMGY4NmRkMDllMThiZmQ3NmFhYmMyNGEwMDgxY2UyODU2ZjMzMDUwNGVkIiwiY29udGVudCI6IiIsImtpbmQiOjI3MjM1LCJjcmVhdGVkX2F0IjoxNjgyMzI3ODUyLCJ0YWdzIjpbWyJ1IiwiaHR0cHM6Ly9hcGkuc25vcnQuc29jaWFsL2FwaS92MS9uNXNwL2xpc3QiXSxbIm1ldGhvZCIsIkdFVCJdXSwic2lnIjoiNWVkOWQ4ZWM5NThiYzg1NGY5OTdiZGMyNGFjMzM3ZDAwNWFmMzcyMzI0NzQ3ZWZlNGEwMGUyNGY0YzMwNDM3ZmY0ZGQ4MzA4Njg0YmVkNDY3ZDlkNmJlM2U1YTUxN2JiNDNiMTczMmNjN2QzMzk0OWEzYWFmODY3MDVjMjIxODQifQ==", &url, HttpMethod::GET, now, None).unwrap_err(), Error::MalformedAuthorizationHeader);
    }

    #[test]
    fn auth_header_wrong_kind() {
        let url = Url::parse("https://example.com/").unwrap();
        let now = Timestamp::now();
        assert_eq!(verify_auth_header("Nostr eyJpZCI6ImZlOTY0ZTc1ODkwMzM2MGYyOGQ4NDI0ZDA5MmRhODQ5NGVkMjA3Y2JhODIzMTEwYmUzYTU3ZGZlNGI1Nzg3MzQiLCJwdWJrZXkiOiI2M2ZlNjMxOGRjNTg1ODNjZmUxNjgxMGY4NmRkMDllMThiZmQ3NmFhYmMyNGEwMDgxY2UyODU2ZjMzMDUwNGVkIiwiY29udGVudCI6IiIsImtpbmQiOjEsImNyZWF0ZWRfYXQiOjE2ODIzMjc4NTIsInRhZ3MiOltbInUiLCJodHRwczovL2FwaS5zbm9ydC5zb2NpYWwvYXBpL3YxL241c3AvbGlzdCJdLFsibWV0aG9kIiwiR0VUIl1dLCJzaWciOiI1ZWQ5ZDhlYzk1OGJjODU0Zjk5N2JkYzI0YWMzMzdkMDA1YWYzNzIzMjQ3NDdlZmU0YTAwZTI0ZjRjMzA0MzdmZjRkZDgzMDg2ODRiZWQ0NjdkOWQ2YmUzZTVhNTE3YmI0M2IxNzMyY2M3ZDMzOTQ5YTNhYWY4NjcwNWMyMjE4NCJ9", &url, HttpMethod::GET, now, None).unwrap_err(), Error::WrongAuthHeaderKind);
    }

    #[test]
    fn auth_header_not_match_request() {
        let url = Url::parse("https://example.com/").unwrap(); // Expected url: https://api.snort.social/api/v1/n5sp/list
        let now = Timestamp::now();
        let method = HttpMethod::POST;
        assert_eq!(verify_auth_header("Nostr eyJpZCI6ImZlOTY0ZTc1ODkwMzM2MGYyOGQ4NDI0ZDA5MmRhODQ5NGVkMjA3Y2JhODIzMTEwYmUzYTU3ZGZlNGI1Nzg3MzQiLCJwdWJrZXkiOiI2M2ZlNjMxOGRjNTg1ODNjZmUxNjgxMGY4NmRkMDllMThiZmQ3NmFhYmMyNGEwMDgxY2UyODU2ZjMzMDUwNGVkIiwiY29udGVudCI6IiIsImtpbmQiOjI3MjM1LCJjcmVhdGVkX2F0IjoxNjgyMzI3ODUyLCJ0YWdzIjpbWyJ1IiwiaHR0cHM6Ly9hcGkuc25vcnQuc29jaWFsL2FwaS92MS9uNXNwL2xpc3QiXSxbIm1ldGhvZCIsIkdFVCJdXSwic2lnIjoiNWVkOWQ4ZWM5NThiYzg1NGY5OTdiZGMyNGFjMzM3ZDAwNWFmMzcyMzI0NzQ3ZWZlNGEwMGUyNGY0YzMwNDM3ZmY0ZGQ4MzA4Njg0YmVkNDY3ZDlkNmJlM2U1YTUxN2JiNDNiMTczMmNjN2QzMzk0OWEzYWFmODY3MDVjMjIxODQifQ==", &url, method, now, None).unwrap_err(), Error::AuthorizationNotMatchRequest {
            authorized_url: Box::new(Url::parse("https://api.snort.social/api/v1/n5sp/list").unwrap()),
            authorized_method: HttpMethod::GET,
            request_url: Box::new(url),
            request_method: HttpMethod::POST,
        });
    }

    #[test]
    fn auth_header_too_old() {
        let url = Url::parse("https://api.snort.social/api/v1/n5sp/list").unwrap();
        let method = HttpMethod::GET;
        let now = Timestamp::from_secs(1777777777);
        assert_eq!(verify_auth_header("Nostr eyJpZCI6ImZlOTY0ZTc1ODkwMzM2MGYyOGQ4NDI0ZDA5MmRhODQ5NGVkMjA3Y2JhODIzMTEwYmUzYTU3ZGZlNGI1Nzg3MzQiLCJwdWJrZXkiOiI2M2ZlNjMxOGRjNTg1ODNjZmUxNjgxMGY4NmRkMDllMThiZmQ3NmFhYmMyNGEwMDgxY2UyODU2ZjMzMDUwNGVkIiwiY29udGVudCI6IiIsImtpbmQiOjI3MjM1LCJjcmVhdGVkX2F0IjoxNjgyMzI3ODUyLCJ0YWdzIjpbWyJ1IiwiaHR0cHM6Ly9hcGkuc25vcnQuc29jaWFsL2FwaS92MS9uNXNwL2xpc3QiXSxbIm1ldGhvZCIsIkdFVCJdXSwic2lnIjoiNWVkOWQ4ZWM5NThiYzg1NGY5OTdiZGMyNGFjMzM3ZDAwNWFmMzcyMzI0NzQ3ZWZlNGEwMGUyNGY0YzMwNDM3ZmY0ZGQ4MzA4Njg0YmVkNDY3ZDlkNmJlM2U1YTUxN2JiNDNiMTczMmNjN2QzMzk0OWEzYWFmODY3MDVjMjIxODQifQ==", &url, method, now, None).unwrap_err(), Error::AuthorizationTooOld {
            current: now,
            created_at: Timestamp::from_secs(1682327852),
        });
    }

    #[test]
    fn valid_auth_header() {
        let url = Url::parse("https://example.com").unwrap();
        let method = HttpMethod::GET;
        let now = Timestamp::from_secs(1742462605);
        let public_key =
            PublicKey::from_hex("aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4")
                .unwrap();
        assert_eq!(verify_auth_header("Nostr eyJpZCI6IjEyYjQ2YmUwMDg5MjI3OWU3YmJhYThlYTg5ODA5ZWNiMWYyYzk5MmY1ZDk0ZWRmMmNkYmQ2Y2JhNmVlMzBjMDMiLCJwdWJrZXkiOiJhYTRmYzg2NjVmNTY5NmUzM2RiN2UxYTU3MmUzYjBmNWIzZDYxNTgzN2IwZjM2MmRjYjFjODA2OGIwOThjN2I0IiwiY3JlYXRlZF9hdCI6MTc0MjQ2MjYwNSwia2luZCI6MjcyMzUsInRhZ3MiOltbInUiLCJodHRwczovL2V4YW1wbGUuY29tLyJdLFsibWV0aG9kIiwiR0VUIl1dLCJjb250ZW50IjoiIiwic2lnIjoiZWEzNGU3NDA3ZGQ2OTFjNDJhYzY3ZjQ3YTMwYjBmMDEwZTFiYWYwMjM3MjhiNzI4OGFlYzA0Zjg3MzMyYmZlYTRhZjJkNDdiNTJiMjhkNGMxMGMwOWY3NmNiZGFhNWZjMTE0OTNiOTlkZTU2NDhmMzlhM2JkYzMwYjQxMTNjNjMifQ==", &url, method, now, None).unwrap(), public_key);
    }
}
