use anyhow::Context;
use axum::http::HeaderMap;
use axum::http::StatusCode;
use axum::http::header::AUTHORIZATION;
use clap::Args;
use clap::ValueEnum;
use codex_utils_absolute_path::AbsolutePathBuf;
use constant_time_eq::constant_time_eq_32;
use jsonwebtoken::Algorithm;
use jsonwebtoken::DecodingKey;
use jsonwebtoken::Validation;
use jsonwebtoken::decode;
use serde::Deserialize;
use sha2::Digest;
use sha2::Sha256;
use std::io;
use std::io::ErrorKind;
use std::net::SocketAddr;
use std::path::Path;
use std::path::PathBuf;
use time::OffsetDateTime;

const DEFAULT_MAX_CLOCK_SKEW_SECONDS: u64 = 30;
const MIN_SIGNED_BEARER_SECRET_BYTES: usize = 32;
const INVALID_AUTHORIZATION_HEADER_MESSAGE: &str = "invalid authorization header";

#[derive(Debug, Clone, Default, PartialEq, Eq, Args)]
pub struct AppServerWebsocketAuthArgs {
    /// Websocket auth mode for non-loopback listeners.
    #[arg(long = "ws-auth", value_name = "MODE", value_enum)]
    pub ws_auth: Option<WebsocketAuthCliMode>,

    /// Absolute path to the capability-token file.
    #[arg(long = "ws-token-file", value_name = "PATH")]
    pub ws_token_file: Option<PathBuf>,

    /// Hex-encoded SHA-256 digest of the capability token.
    #[arg(long = "ws-token-sha256", value_name = "HEX")]
    pub ws_token_sha256: Option<String>,

    /// Absolute path to the shared secret file for signed JWT bearer tokens.
    #[arg(long = "ws-shared-secret-file", value_name = "PATH")]
    pub ws_shared_secret_file: Option<PathBuf>,

    /// Expected issuer for signed JWT bearer tokens.
    #[arg(long = "ws-issuer", value_name = "ISSUER")]
    pub ws_issuer: Option<String>,

    /// Expected audience for signed JWT bearer tokens.
    #[arg(long = "ws-audience", value_name = "AUDIENCE")]
    pub ws_audience: Option<String>,

    /// Maximum clock skew when validating signed JWT bearer tokens.
    #[arg(long = "ws-max-clock-skew-seconds", value_name = "SECONDS")]
    pub ws_max_clock_skew_seconds: Option<u64>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum WebsocketAuthCliMode {
    CapabilityToken,
    SignedBearerToken,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AppServerWebsocketAuthSettings {
    pub config: Option<AppServerWebsocketAuthConfig>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppServerWebsocketAuthConfig {
    CapabilityToken {
        source: AppServerWebsocketCapabilityTokenSource,
    },
    SignedBearerToken {
        shared_secret_file: AbsolutePathBuf,
        issuer: Option<String>,
        audience: Option<String>,
        max_clock_skew_seconds: u64,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppServerWebsocketCapabilityTokenSource {
    TokenFile { token_file: AbsolutePathBuf },
    TokenSha256 { token_sha256: [u8; 32] },
}

#[derive(Clone, Debug, Default)]
pub struct WebsocketAuthPolicy {
    pub(crate) mode: Option<WebsocketAuthMode>,
}

#[derive(Clone, Debug)]
pub(crate) enum WebsocketAuthMode {
    CapabilityToken {
        token_sha256: [u8; 32],
    },
    SignedBearerToken {
        shared_secret: Vec<u8>,
        issuer: Option<String>,
        audience: Option<String>,
        max_clock_skew_seconds: i64,
    },
}

#[derive(Debug)]
pub(crate) struct WebsocketAuthError {
    status_code: StatusCode,
    message: &'static str,
}

#[derive(Deserialize)]
struct JwtClaims {
    exp: i64,
    nbf: Option<i64>,
    iss: Option<String>,
    aud: Option<JwtAudienceClaim>,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum JwtAudienceClaim {
    Single(String),
    Multiple(Vec<String>),
}

impl WebsocketAuthError {
    pub(crate) fn status_code(&self) -> StatusCode {
        self.status_code
    }

    pub(crate) fn message(&self) -> &'static str {
        self.message
    }
}

impl AppServerWebsocketAuthArgs {
    pub fn try_into_settings(self) -> anyhow::Result<AppServerWebsocketAuthSettings> {
        let normalize = |value: Option<String>| {
            value.and_then(|value| {
                let trimmed = value.trim();
                (!trimmed.is_empty()).then(|| trimmed.to_string())
            })
        };

        let config = match self.ws_auth {
            Some(WebsocketAuthCliMode::CapabilityToken) => {
                if self.ws_shared_secret_file.is_some()
                    || self.ws_issuer.is_some()
                    || self.ws_audience.is_some()
                    || self.ws_max_clock_skew_seconds.is_some()
                {
                    anyhow::bail!(
                        "`--ws-shared-secret-file`, `--ws-issuer`, `--ws-audience`, and `--ws-max-clock-skew-seconds` require `--ws-auth signed-bearer-token`"
                    );
                }
                let source = match (self.ws_token_file, self.ws_token_sha256) {
                    (Some(_), Some(_)) => {
                        anyhow::bail!(
                            "`--ws-token-file` and `--ws-token-sha256` are mutually exclusive"
                        );
                    }
                    (Some(token_file), None) => {
                        AppServerWebsocketCapabilityTokenSource::TokenFile {
                            token_file: absolute_path_arg("--ws-token-file", token_file)?,
                        }
                    }
                    (None, Some(token_sha256)) => {
                        AppServerWebsocketCapabilityTokenSource::TokenSha256 {
                            token_sha256: sha256_digest_arg("--ws-token-sha256", &token_sha256)?,
                        }
                    }
                    (None, None) => {
                        anyhow::bail!(
                            "`--ws-token-file` or `--ws-token-sha256` is required when `--ws-auth capability-token` is set"
                        );
                    }
                };
                Some(AppServerWebsocketAuthConfig::CapabilityToken { source })
            }
            Some(WebsocketAuthCliMode::SignedBearerToken) => {
                if self.ws_token_file.is_some() || self.ws_token_sha256.is_some() {
                    anyhow::bail!(
                        "`--ws-token-file` and `--ws-token-sha256` require `--ws-auth capability-token`, not `signed-bearer-token`"
                    );
                }
                let shared_secret_file = self.ws_shared_secret_file.context(
                    "`--ws-shared-secret-file` is required when `--ws-auth signed-bearer-token` is set",
                )?;
                Some(AppServerWebsocketAuthConfig::SignedBearerToken {
                    shared_secret_file: absolute_path_arg(
                        "--ws-shared-secret-file",
                        shared_secret_file,
                    )?,
                    issuer: normalize(self.ws_issuer),
                    audience: normalize(self.ws_audience),
                    max_clock_skew_seconds: self
                        .ws_max_clock_skew_seconds
                        .unwrap_or(DEFAULT_MAX_CLOCK_SKEW_SECONDS),
                })
            }
            None => {
                if self.ws_token_file.is_some()
                    || self.ws_token_sha256.is_some()
                    || self.ws_shared_secret_file.is_some()
                    || self.ws_issuer.is_some()
                    || self.ws_audience.is_some()
                    || self.ws_max_clock_skew_seconds.is_some()
                {
                    anyhow::bail!(
                        "websocket auth flags require `--ws-auth capability-token` or `--ws-auth signed-bearer-token`"
                    );
                }
                None
            }
        };

        Ok(AppServerWebsocketAuthSettings { config })
    }
}

pub fn policy_from_settings(
    settings: &AppServerWebsocketAuthSettings,
) -> io::Result<WebsocketAuthPolicy> {
    let mode = match settings.config.as_ref() {
        Some(AppServerWebsocketAuthConfig::CapabilityToken { source }) => match source {
            AppServerWebsocketCapabilityTokenSource::TokenFile { token_file } => {
                let token = read_trimmed_secret(token_file.as_ref())?;
                Some(WebsocketAuthMode::CapabilityToken {
                    token_sha256: sha256_digest(token.as_bytes()),
                })
            }
            AppServerWebsocketCapabilityTokenSource::TokenSha256 { token_sha256 } => {
                Some(WebsocketAuthMode::CapabilityToken {
                    token_sha256: *token_sha256,
                })
            }
        },
        Some(AppServerWebsocketAuthConfig::SignedBearerToken {
            shared_secret_file,
            issuer,
            audience,
            max_clock_skew_seconds,
        }) => {
            let shared_secret = read_trimmed_secret(shared_secret_file.as_ref())?.into_bytes();
            validate_signed_bearer_secret(shared_secret_file.as_ref(), &shared_secret)?;
            let max_clock_skew_seconds = i64::try_from(*max_clock_skew_seconds).map_err(|_| {
                io::Error::new(
                    ErrorKind::InvalidInput,
                    "websocket auth clock skew must fit in a signed 64-bit integer",
                )
            })?;
            Some(WebsocketAuthMode::SignedBearerToken {
                shared_secret,
                issuer: issuer.clone(),
                audience: audience.clone(),
                max_clock_skew_seconds,
            })
        }
        None => None,
    };

    Ok(WebsocketAuthPolicy { mode })
}

pub(crate) fn should_warn_about_unauthenticated_non_loopback_listener(
    bind_address: SocketAddr,
    policy: &WebsocketAuthPolicy,
) -> bool {
    !bind_address.ip().is_loopback() && policy.mode.is_none()
}

pub(crate) fn authorize_upgrade(
    headers: &HeaderMap,
    policy: &WebsocketAuthPolicy,
) -> Result<(), WebsocketAuthError> {
    let Some(mode) = policy.mode.as_ref() else {
        return Ok(());
    };

    let token = bearer_token_from_headers(headers)?;
    match mode {
        WebsocketAuthMode::CapabilityToken { token_sha256 } => {
            let actual_sha256 = sha256_digest(token.as_bytes());
            if constant_time_eq_32(token_sha256, &actual_sha256) {
                Ok(())
            } else {
                Err(unauthorized("invalid websocket bearer token"))
            }
        }
        WebsocketAuthMode::SignedBearerToken {
            shared_secret,
            issuer,
            audience,
            max_clock_skew_seconds,
        } => verify_signed_bearer_token(
            token,
            shared_secret,
            issuer.as_deref(),
            audience.as_deref(),
            *max_clock_skew_seconds,
        ),
    }
}

fn verify_signed_bearer_token(
    token: &str,
    shared_secret: &[u8],
    issuer: Option<&str>,
    audience: Option<&str>,
    max_clock_skew_seconds: i64,
) -> Result<(), WebsocketAuthError> {
    let claims = decode_jwt_claims(token, shared_secret)?;
    validate_jwt_claims(&claims, issuer, audience, max_clock_skew_seconds)
}

fn decode_jwt_claims(token: &str, shared_secret: &[u8]) -> Result<JwtClaims, WebsocketAuthError> {
    let mut validation = Validation::new(Algorithm::HS256);
    validation.required_spec_claims.clear();
    validation.validate_exp = false;
    validation.validate_nbf = false;
    validation.validate_aud = false;

    decode::<JwtClaims>(token, &DecodingKey::from_secret(shared_secret), &validation)
        .map(|token_data| token_data.claims)
        .map_err(|_| unauthorized("invalid websocket jwt"))
}

fn validate_jwt_claims(
    claims: &JwtClaims,
    issuer: Option<&str>,
    audience: Option<&str>,
    max_clock_skew_seconds: i64,
) -> Result<(), WebsocketAuthError> {
    let now = OffsetDateTime::now_utc().unix_timestamp();
    if now > claims.exp.saturating_add(max_clock_skew_seconds) {
        return Err(unauthorized("expired websocket jwt"));
    }
    if let Some(nbf) = claims.nbf
        && now < nbf.saturating_sub(max_clock_skew_seconds)
    {
        return Err(unauthorized("websocket jwt is not valid yet"));
    }
    if let Some(expected_issuer) = issuer
        && claims.iss.as_deref() != Some(expected_issuer)
    {
        return Err(unauthorized("websocket jwt issuer mismatch"));
    }
    if let Some(expected_audience) = audience
        && !audience_matches(claims.aud.as_ref(), expected_audience)
    {
        return Err(unauthorized("websocket jwt audience mismatch"));
    }

    Ok(())
}

fn audience_matches(audience: Option<&JwtAudienceClaim>, expected_audience: &str) -> bool {
    match audience {
        Some(JwtAudienceClaim::Single(actual)) => actual == expected_audience,
        Some(JwtAudienceClaim::Multiple(actual)) => {
            actual.iter().any(|audience| audience == expected_audience)
        }
        None => false,
    }
}

fn bearer_token_from_headers(headers: &HeaderMap) -> Result<&str, WebsocketAuthError> {
    let raw_header = headers
        .get(AUTHORIZATION)
        .ok_or_else(|| unauthorized("missing websocket bearer token"))?;
    let header = raw_header
        .to_str()
        .map_err(|_| unauthorized(INVALID_AUTHORIZATION_HEADER_MESSAGE))?;
    let Some((scheme, token)) = header.split_once(' ') else {
        return Err(unauthorized(INVALID_AUTHORIZATION_HEADER_MESSAGE));
    };
    if !scheme.eq_ignore_ascii_case("Bearer") {
        return Err(unauthorized(INVALID_AUTHORIZATION_HEADER_MESSAGE));
    }
    let token = token.trim();
    if token.is_empty() {
        return Err(unauthorized(INVALID_AUTHORIZATION_HEADER_MESSAGE));
    }
    Ok(token)
}

fn validate_signed_bearer_secret(path: &Path, shared_secret: &[u8]) -> io::Result<()> {
    if shared_secret.len() < MIN_SIGNED_BEARER_SECRET_BYTES {
        return Err(io::Error::new(
            ErrorKind::InvalidInput,
            format!(
                "signed websocket bearer secret {} must be at least {MIN_SIGNED_BEARER_SECRET_BYTES} bytes",
                path.display()
            ),
        ));
    }
    Ok(())
}

fn read_trimmed_secret(path: &std::path::Path) -> io::Result<String> {
    let raw = std::fs::read_to_string(path).map_err(|err| {
        io::Error::new(
            err.kind(),
            format!(
                "failed to read websocket auth secret {}: {err}",
                path.display()
            ),
        )
    })?;
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Err(io::Error::new(
            ErrorKind::InvalidInput,
            format!("websocket auth secret {} must not be empty", path.display()),
        ));
    }
    Ok(trimmed.to_string())
}

fn absolute_path_arg(flag_name: &str, path: PathBuf) -> anyhow::Result<AbsolutePathBuf> {
    AbsolutePathBuf::try_from(path).with_context(|| format!("{flag_name} must be an absolute path"))
}

fn sha256_digest_arg(flag_name: &str, value: &str) -> anyhow::Result<[u8; 32]> {
    let trimmed = value.trim();
    if trimmed.len() != 64 {
        anyhow::bail!("{flag_name} must be a 64-character hex SHA-256 digest");
    }

    let mut digest = [0u8; 32];
    for (index, pair) in trimmed.as_bytes().chunks_exact(2).enumerate() {
        let high = hex_nibble(flag_name, pair[0])?;
        let low = hex_nibble(flag_name, pair[1])?;
        digest[index] = (high << 4) | low;
    }
    Ok(digest)
}

fn hex_nibble(flag_name: &str, byte: u8) -> anyhow::Result<u8> {
    match byte {
        b'0'..=b'9' => Ok(byte - b'0'),
        b'a'..=b'f' => Ok(byte - b'a' + 10),
        b'A'..=b'F' => Ok(byte - b'A' + 10),
        _ => anyhow::bail!("{flag_name} must be a 64-character hex SHA-256 digest"),
    }
}

fn sha256_digest(input: &[u8]) -> [u8; 32] {
    let mut digest = [0u8; 32];
    digest.copy_from_slice(&Sha256::digest(input));
    digest
}

fn unauthorized(message: &'static str) -> WebsocketAuthError {
    WebsocketAuthError {
        status_code: StatusCode::UNAUTHORIZED,
        message,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::http::HeaderValue;
    use base64::Engine;
    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
    use hmac::Hmac;
    use hmac::Mac;
    use serde_json::json;

    type HmacSha256 = Hmac<Sha256>;

    fn signed_token(shared_secret: &[u8], claims: serde_json::Value) -> String {
        let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"HS256","typ":"JWT"}"#);
        let claims_segment = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).unwrap());
        let payload = format!("{header}.{claims_segment}");
        let mut mac = HmacSha256::new_from_slice(shared_secret).unwrap();
        mac.update(payload.as_bytes());
        let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
        format!("{payload}.{signature}")
    }

    #[test]
    fn warns_about_unauthenticated_non_loopback_listener() {
        let policy = WebsocketAuthPolicy::default();
        assert!(should_warn_about_unauthenticated_non_loopback_listener(
            "0.0.0.0:8765".parse().unwrap(),
            &policy,
        ));
        assert!(!should_warn_about_unauthenticated_non_loopback_listener(
            "127.0.0.1:8765".parse().unwrap(),
            &policy,
        ));
        assert!(!should_warn_about_unauthenticated_non_loopback_listener(
            "0.0.0.0:8765".parse().unwrap(),
            &WebsocketAuthPolicy {
                mode: Some(WebsocketAuthMode::CapabilityToken {
                    token_sha256: [0u8; 32],
                }),
            },
        ));
    }

    #[test]
    fn capability_token_args_require_token_file_or_hash() {
        let err = AppServerWebsocketAuthArgs {
            ws_auth: Some(WebsocketAuthCliMode::CapabilityToken),
            ..Default::default()
        }
        .try_into_settings()
        .expect_err("capability-token mode should require a token source");
        assert!(
            err.to_string().contains("--ws-token-file")
                && err.to_string().contains("--ws-token-sha256"),
            "unexpected error: {err}"
        );
    }

    #[test]
    fn capability_token_args_accept_token_hash() {
        let settings = AppServerWebsocketAuthArgs {
            ws_auth: Some(WebsocketAuthCliMode::CapabilityToken),
            ws_token_sha256: Some("ab".repeat(32)),
            ..Default::default()
        }
        .try_into_settings()
        .expect("capability-token hash args should parse");

        assert_eq!(
            settings,
            AppServerWebsocketAuthSettings {
                config: Some(AppServerWebsocketAuthConfig::CapabilityToken {
                    source: AppServerWebsocketCapabilityTokenSource::TokenSha256 {
                        token_sha256: [0xab; 32],
                    },
                }),
            }
        );
    }

    #[test]
    fn capability_token_args_reject_multiple_token_sources() {
        let err = AppServerWebsocketAuthArgs {
            ws_auth: Some(WebsocketAuthCliMode::CapabilityToken),
            ws_token_file: Some(PathBuf::from("/tmp/token")),
            ws_token_sha256: Some("ab".repeat(32)),
            ..Default::default()
        }
        .try_into_settings()
        .expect_err("capability-token mode should reject multiple token sources");
        assert!(
            err.to_string().contains("mutually exclusive"),
            "unexpected error: {err}"
        );
    }

    #[test]
    fn capability_token_args_reject_malformed_token_hash() {
        let err = AppServerWebsocketAuthArgs {
            ws_auth: Some(WebsocketAuthCliMode::CapabilityToken),
            ws_token_sha256: Some("not-a-sha256".to_string()),
            ..Default::default()
        }
        .try_into_settings()
        .expect_err("capability-token mode should reject malformed token hashes");
        assert!(
            err.to_string().contains("64-character hex"),
            "unexpected error: {err}"
        );
    }

    #[test]
    fn capability_token_hash_policy_authorizes_matching_bearer_token() {
        let settings = AppServerWebsocketAuthSettings {
            config: Some(AppServerWebsocketAuthConfig::CapabilityToken {
                source: AppServerWebsocketCapabilityTokenSource::TokenSha256 {
                    token_sha256: sha256_digest(b"super-secret-token"),
                },
            }),
        };
        let policy = policy_from_settings(&settings).expect("hash policy should build");
        let mut headers = HeaderMap::new();
        headers.insert(
            AUTHORIZATION,
            HeaderValue::from_static("Bearer super-secret-token"),
        );
        authorize_upgrade(&headers, &policy).expect("matching token should authorize");

        headers.insert(
            AUTHORIZATION,
            HeaderValue::from_static("Bearer wrong-token"),
        );
        let err = authorize_upgrade(&headers, &policy).expect_err("wrong token should fail");
        assert_eq!(err.status_code(), StatusCode::UNAUTHORIZED);
    }

    #[test]
    fn signed_bearer_args_require_mode_when_mode_specific_flags_are_set() {
        let err = AppServerWebsocketAuthArgs {
            ws_shared_secret_file: Some(PathBuf::from("/tmp/secret")),
            ..Default::default()
        }
        .try_into_settings()
        .expect_err("mode-specific flags should require --ws-auth");
        assert!(
            err.to_string().contains("websocket auth flags require"),
            "unexpected error: {err}"
        );
    }

    #[test]
    fn signed_bearer_args_default_clock_skew_and_trim_optional_claims() {
        let settings = AppServerWebsocketAuthArgs {
            ws_auth: Some(WebsocketAuthCliMode::SignedBearerToken),
            ws_shared_secret_file: Some(PathBuf::from("/tmp/secret")),
            ws_issuer: Some(" issuer ".to_string()),
            ws_audience: Some("   ".to_string()),
            ..Default::default()
        }
        .try_into_settings()
        .expect("signed bearer args should parse");

        assert_eq!(
            settings,
            AppServerWebsocketAuthSettings {
                config: Some(AppServerWebsocketAuthConfig::SignedBearerToken {
                    shared_secret_file: AbsolutePathBuf::from_absolute_path("/tmp/secret")
                        .expect("absolute path"),
                    issuer: Some("issuer".to_string()),
                    audience: None,
                    max_clock_skew_seconds: DEFAULT_MAX_CLOCK_SKEW_SECONDS,
                }),
            }
        );
    }

    #[test]
    fn signed_bearer_token_verification_rejects_tampering() {
        let shared_secret = b"0123456789abcdef0123456789abcdef";
        let token = signed_token(
            shared_secret,
            json!({
                "exp": OffsetDateTime::now_utc().unix_timestamp() + 60,
            }),
        );
        let tampered = token.replace(".eyJleHAi", ".eyJleHBi");
        let err = verify_signed_bearer_token(
            &tampered,
            shared_secret,
            /*issuer*/ None,
            /*audience*/ None,
            /*max_clock_skew_seconds*/ 30,
        )
        .expect_err("tampered jwt should fail");
        assert_eq!(err.status_code(), StatusCode::UNAUTHORIZED);
    }

    #[test]
    fn signed_bearer_token_verification_accepts_valid_token() {
        let shared_secret = b"0123456789abcdef0123456789abcdef";
        let token = signed_token(
            shared_secret,
            json!({
                "exp": OffsetDateTime::now_utc().unix_timestamp() + 60,
                "iss": "issuer",
                "aud": "audience",
            }),
        );
        verify_signed_bearer_token(
            &token,
            shared_secret,
            Some("issuer"),
            Some("audience"),
            /*max_clock_skew_seconds*/ 30,
        )
        .expect("valid signed token should verify");
    }

    #[test]
    fn signed_bearer_token_verification_accepts_multiple_audiences() {
        let shared_secret = b"0123456789abcdef0123456789abcdef";
        let token = signed_token(
            shared_secret,
            json!({
                "exp": OffsetDateTime::now_utc().unix_timestamp() + 60,
                "aud": ["other-audience", "audience"],
            }),
        );
        verify_signed_bearer_token(
            &token,
            shared_secret,
            /*issuer*/ None,
            Some("audience"),
            /*max_clock_skew_seconds*/ 30,
        )
        .expect("jwt audience arrays should verify");
    }

    #[test]
    fn signed_bearer_token_verification_rejects_alg_none_tokens() {
        let claims_segment = URL_SAFE_NO_PAD.encode(
            serde_json::to_vec(&json!({
                "exp": OffsetDateTime::now_utc().unix_timestamp() + 60,
            }))
            .unwrap(),
        );
        let header_segment = URL_SAFE_NO_PAD.encode(br#"{"alg":"none","typ":"JWT"}"#);
        let token = format!("{header_segment}.{claims_segment}.");
        let err = verify_signed_bearer_token(
            &token,
            b"0123456789abcdef0123456789abcdef",
            /*issuer*/ None,
            /*audience*/ None,
            /*max_clock_skew_seconds*/ 30,
        )
        .expect_err("alg=none jwt should be rejected");
        assert_eq!(err.status_code(), StatusCode::UNAUTHORIZED);
    }

    #[test]
    fn signed_bearer_token_verification_rejects_missing_exp() {
        let shared_secret = b"0123456789abcdef0123456789abcdef";
        let token = signed_token(
            shared_secret,
            json!({
                "iss": "issuer",
            }),
        );
        let err = verify_signed_bearer_token(
            &token,
            shared_secret,
            /*issuer*/ None,
            /*audience*/ None,
            /*max_clock_skew_seconds*/ 30,
        )
        .expect_err("jwt without exp should be rejected");
        assert_eq!(err.status_code(), StatusCode::UNAUTHORIZED);
    }

    #[test]
    fn validate_signed_bearer_secret_rejects_short_secret() {
        let err = validate_signed_bearer_secret(Path::new("/tmp/secret"), b"too-short")
            .expect_err("short shared secret should be rejected");
        assert_eq!(err.kind(), ErrorKind::InvalidInput);
        assert!(
            err.to_string().contains("must be at least 32 bytes"),
            "unexpected error: {err}"
        );
    }
}
