use axum::http::{HeaderMap, HeaderName};
use base64::Engine as _;

use cid::Cid;
use ipld_core::ipld::Ipld;
use rand::Rng;
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::str::FromStr;
use std::sync::OnceLock;

const BASE64_STANDARD_INDIFFERENT: base64::engine::GeneralPurpose =
    base64::engine::GeneralPurpose::new(
        &base64::alphabet::STANDARD,
        base64::engine::GeneralPurposeConfig::new()
            .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
    );

const BASE32_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz234567";

static DISCORD_BOT_USERNAME: OnceLock<String> = OnceLock::new();
static DISCORD_PUBLIC_KEY: OnceLock<ed25519_dalek::VerifyingKey> = OnceLock::new();
static DISCORD_APP_ID: OnceLock<String> = OnceLock::new();
static TELEGRAM_BOT_USERNAME: OnceLock<String> = OnceLock::new();

pub fn generate_token_code() -> String {
    let mut rng = rand::thread_rng();
    let chars: Vec<char> = BASE32_ALPHABET.chars().collect();
    let gen_segment = |rng: &mut rand::rngs::ThreadRng| -> String {
        (0..5)
            .map(|_| chars[rng.gen_range(0..chars.len())])
            .collect()
    };
    // Human-entered short codes are displayed in uppercase; base32 digits are
    // unaffected by the conversion.
    format!("{}-{}", gen_segment(&mut rng), gen_segment(&mut rng)).to_uppercase()
}

/// Normalize a user-entered short code so that codes are accepted
/// case-insensitively and regardless of the separating hyphen or surrounding
/// whitespace.
pub fn normalize_token_code(input: &str) -> String {
    input
        .chars()
        .filter(|c| !c.is_whitespace() && *c != '-')
        .collect::<String>()
        .to_uppercase()
}

pub fn parse_repeated_query_param(query: Option<&str>, key: &str) -> Vec<String> {
    query
        .map(|q| {
            q.split('&')
                .filter_map(|pair| {
                    pair.split_once('=')
                        .filter(|(k, _)| *k == key)
                        .and_then(|(_, v)| urlencoding::decode(v).ok())
                        .map(|decoded| decoded.into_owned())
                })
                .flat_map(|decoded| {
                    if decoded.contains(',') {
                        decoded
                            .split(',')
                            .filter_map(|part| {
                                let trimmed = part.trim();
                                (!trimmed.is_empty()).then(|| trimmed.to_string())
                            })
                            .collect::<Vec<_>>()
                    } else if decoded.is_empty() {
                        vec![]
                    } else {
                        vec![decoded]
                    }
                })
                .collect()
        })
        .unwrap_or_default()
}

pub const HEADER_DPOP: HeaderName = HeaderName::from_static("dpop");
pub const HEADER_DPOP_NONCE: HeaderName = HeaderName::from_static("dpop-nonce");
pub const HEADER_ATPROTO_PROXY: HeaderName = HeaderName::from_static("atproto-proxy");
pub const HEADER_ATPROTO_ACCEPT_LABELERS: HeaderName =
    HeaderName::from_static("atproto-accept-labelers");
pub const HEADER_ATPROTO_REPO_REV: HeaderName = HeaderName::from_static("atproto-repo-rev");
pub const HEADER_ATPROTO_CONTENT_LABELERS: HeaderName =
    HeaderName::from_static("atproto-content-labelers");
#[cfg(feature = "bsky-support")]
pub const HEADER_X_BSKY_TOPICS: HeaderName = HeaderName::from_static("x-bsky-topics");

pub fn get_header_str(
    headers: &HeaderMap,
    name: impl axum::http::header::AsHeaderName,
) -> Option<&str> {
    headers.get(name).and_then(|h| h.to_str().ok())
}

pub fn extract_user_agent(headers: &HeaderMap) -> Option<String> {
    headers
        .get("user-agent")
        .and_then(|v| v.to_str().ok())
        .map(|s| s.to_string())
}

pub fn generate_random_token() -> String {
    use base64::Engine as _;
    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
    let bytes: [u8; 32] = rand::thread_rng().r#gen();
    URL_SAFE_NO_PAD.encode(bytes)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ForwardedTrust {
    Peer,
    Proxies(NonZeroUsize),
}

fn resolve_trust(configured: Option<usize>, terminates_tls: bool) -> ForwardedTrust {
    let count = configured.unwrap_or(if terminates_tls { 0 } else { 1 });
    match NonZeroUsize::new(count) {
        Some(proxies) => ForwardedTrust::Proxies(proxies),
        None => ForwardedTrust::Peer,
    }
}

pub(crate) fn forwarded_trust() -> ForwardedTrust {
    match tranquil_config::try_get() {
        Some(cfg) => resolve_trust(
            cfg.server.trusted_proxy_count,
            cfg.server.tls.material().is_some(),
        ),
        None => ForwardedTrust::Peer,
    }
}

fn forwarded_client_ip(headers: &HeaderMap, trusted: NonZeroUsize) -> Option<String> {
    if let Some(forwarded) = headers.get("x-forwarded-for")
        && let Ok(value) = forwarded.to_str()
    {
        let hops: Vec<&str> = value
            .split(',')
            .map(str::trim)
            .filter(|s| !s.is_empty())
            .collect();
        if let Some(client) = hops
            .len()
            .checked_sub(trusted.get())
            .and_then(|idx| hops.get(idx))
        {
            return Some((*client).to_string());
        }
    }
    if trusted.get() == 1
        && let Some(real_ip) = headers.get("x-real-ip")
        && let Ok(value) = real_ip.to_str()
        && !value.trim().is_empty()
    {
        return Some(value.trim().to_string());
    }
    None
}

pub(crate) fn extract_client_ip(
    headers: &HeaderMap,
    addr: Option<SocketAddr>,
    trust: ForwardedTrust,
) -> String {
    if let ForwardedTrust::Proxies(trusted) = trust
        && let Some(client) = forwarded_client_ip(headers, trusted)
    {
        return client;
    }
    addr.map(|a| a.ip().to_string())
        .unwrap_or_else(|| "unknown".to_string())
}

pub(crate) fn client_ip_from_parts(parts: &axum::http::request::Parts) -> String {
    let addr = parts
        .extensions
        .get::<axum::extract::ConnectInfo<SocketAddr>>()
        .map(|connect_info| connect_info.0);
    extract_client_ip(&parts.headers, addr, forwarded_trust())
}

#[derive(Debug, Clone)]
pub struct ClientIp(String);

impl ClientIp {
    pub fn into_string(self) -> String {
        self.0
    }
}

impl<S: Send + Sync> axum::extract::FromRequestParts<S> for ClientIp {
    type Rejection = std::convert::Infallible;

    async fn from_request_parts(
        parts: &mut axum::http::request::Parts,
        _state: &S,
    ) -> Result<Self, Self::Rejection> {
        Ok(ClientIp(client_ip_from_parts(parts)))
    }
}

pub fn set_discord_bot_username(username: String) {
    DISCORD_BOT_USERNAME.set(username).ok();
}

pub fn discord_bot_username() -> Option<&'static str> {
    DISCORD_BOT_USERNAME.get().map(|s| s.as_str())
}

pub fn set_discord_public_key(key: ed25519_dalek::VerifyingKey) {
    DISCORD_PUBLIC_KEY.set(key).ok();
}

pub fn discord_public_key() -> Option<&'static ed25519_dalek::VerifyingKey> {
    DISCORD_PUBLIC_KEY.get()
}

pub fn set_discord_app_id(app_id: String) {
    DISCORD_APP_ID.set(app_id).ok();
}

pub fn discord_app_id() -> Option<&'static str> {
    DISCORD_APP_ID.get().map(|s| s.as_str())
}

pub fn set_telegram_bot_username(username: String) {
    TELEGRAM_BOT_USERNAME.set(username).ok();
}

pub fn telegram_bot_username() -> Option<&'static str> {
    TELEGRAM_BOT_USERNAME.get().map(|s| s.as_str())
}

pub fn parse_env_bool(key: &str) -> bool {
    // Check the config system first, then fall back to env var for dynamic
    // SSO keys that are not in the static config struct.
    std::env::var(key)
        .map(|v| v == "true" || v == "1")
        .unwrap_or(false)
}

pub fn build_full_url(path: &str) -> String {
    let cfg = tranquil_config::get();
    let normalized_path = if !path.starts_with("/xrpc/")
        && (path.starts_with("/com.atproto.")
            // BSKY: Bluesky requires that the PDS implement some app.bsky.* endpoints so we need to deal with those here too.
            // TODO: surely we can figure out a way to do this more generically?
            || (cfg!(feature = "bsky-support") && path.starts_with("/app.bsky."))
            || path.starts_with("/_"))
    {
        format!("/xrpc{path}")
    } else {
        path.to_string()
    };
    format!("{}{normalized_path}", cfg.server.public_url())
}

pub fn json_to_ipld(value: &JsonValue) -> Ipld {
    match value {
        JsonValue::Null => Ipld::Null,
        JsonValue::Bool(b) => Ipld::Bool(*b),
        JsonValue::Number(n) => {
            if let Some(i) = n.as_i64() {
                Ipld::Integer(i128::from(i))
            } else if let Some(f) = n.as_f64() {
                Ipld::Float(f)
            } else {
                Ipld::Null
            }
        }
        JsonValue::String(s) => Ipld::String(s.clone()),
        JsonValue::Array(arr) => Ipld::List(arr.iter().map(json_to_ipld).collect()),
        JsonValue::Object(obj) => {
            if let Some(JsonValue::String(link)) = obj.get("$link")
                && obj.len() == 1
                && let Ok(cid) = Cid::from_str(link)
            {
                return Ipld::Link(cid);
            }
            if let Some(JsonValue::String(b64)) = obj.get("$bytes")
                && obj.len() == 1
                && let Ok(bytes) = BASE64_STANDARD_INDIFFERENT.decode(b64)
            {
                return Ipld::Bytes(bytes);
            }
            let map: BTreeMap<String, Ipld> = obj
                .iter()
                .map(|(k, v)| (k.clone(), json_to_ipld(v)))
                .collect();
            Ipld::Map(map)
        }
    }
}

pub(crate) fn gen_invite_random_token() -> String {
    let mut rng = rand::thread_rng();
    let chars: Vec<char> = BASE32_ALPHABET.chars().collect();
    let gen_segment = |rng: &mut rand::rngs::ThreadRng, len: usize| -> String {
        (0..len)
            .map(|_| chars[rng.gen_range(0..chars.len())])
            .collect()
    };
    format!("{}-{}", gen_segment(&mut rng, 5), gen_segment(&mut rng, 5))
}

pub fn gen_invite_code() -> crate::types::InviteCode {
    let hostname = &tranquil_config::get().server.hostname;
    let hostname_prefix = hostname.replace('.', "-");
    crate::types::InviteCode::from(format!("{}-{}", hostname_prefix, gen_invite_random_token()))
}

pub fn is_self_hosted_did_web_enabled() -> bool {
    tranquil_config::get().server.enable_pds_hosted_did_web
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::extract::{ConnectInfo, FromRequestParts};

    fn proxies(count: usize) -> ForwardedTrust {
        ForwardedTrust::Proxies(NonZeroUsize::new(count).unwrap())
    }

    #[test]
    fn resolve_trust_override_wins_over_tls() {
        assert_eq!(resolve_trust(Some(1), true), proxies(1));
        assert_eq!(resolve_trust(Some(3), false), proxies(3));
        assert_eq!(resolve_trust(Some(0), true), ForwardedTrust::Peer);
        assert_eq!(resolve_trust(Some(0), false), ForwardedTrust::Peer);
    }

    #[test]
    fn resolve_trust_infers_from_tls_when_unset() {
        assert_eq!(resolve_trust(None, true), ForwardedTrust::Peer);
        assert_eq!(resolve_trust(None, false), proxies(1));
    }

    fn parts_with(
        header: Option<(&str, &str)>,
        peer: Option<SocketAddr>,
    ) -> axum::http::request::Parts {
        let mut builder = axum::http::Request::builder();
        if let Some((name, value)) = header {
            builder = builder.header(name, value);
        }
        let mut parts = builder.body(()).unwrap().into_parts().0;
        if let Some(addr) = peer {
            parts.extensions.insert(ConnectInfo(addr));
        }
        parts
    }

    #[tokio::test]
    async fn client_ip_falls_back_to_peer_socket() {
        let peer: SocketAddr = "203.0.113.7:51000".parse().unwrap();
        let mut parts = parts_with(None, Some(peer));
        let ip = ClientIp::from_request_parts(&mut parts, &()).await.unwrap();
        assert_eq!(ip.into_string(), "203.0.113.7");
    }

    #[tokio::test]
    async fn client_ip_ignores_forwarded_when_config_absent() {
        let peer: SocketAddr = "203.0.113.7:51000".parse().unwrap();
        let mut parts = parts_with(
            Some(("x-forwarded-for", "198.51.100.4, 10.0.0.1")),
            Some(peer),
        );
        let ip = ClientIp::from_request_parts(&mut parts, &()).await.unwrap();
        assert_eq!(ip.into_string(), "203.0.113.7");
    }

    #[tokio::test]
    async fn client_ip_unknown_without_headers_or_peer() {
        let mut parts = parts_with(None, None);
        let ip = ClientIp::from_request_parts(&mut parts, &()).await.unwrap();
        assert_eq!(ip.into_string(), "unknown");
    }

    #[tokio::test]
    async fn client_ip_renders_ipv6_peer_without_brackets() {
        let peer: SocketAddr = "[2001:db8::beef]:51000".parse().unwrap();
        let mut parts = parts_with(None, Some(peer));
        let ip = ClientIp::from_request_parts(&mut parts, &()).await.unwrap();
        assert_eq!(ip.into_string(), "2001:db8::beef");
    }

    #[test]
    fn extract_client_ip_single_proxy_takes_rightmost_forwarded_hop() {
        let mut headers = HeaderMap::new();
        headers.insert(
            "x-forwarded-for",
            "9.9.9.9, 198.51.100.4, 10.0.0.1".parse().unwrap(),
        );
        let peer: SocketAddr = "203.0.113.7:51000".parse().unwrap();
        assert_eq!(
            extract_client_ip(&headers, Some(peer), proxies(1)),
            "10.0.0.1"
        );
    }

    #[test]
    fn extract_client_ip_two_proxies_skips_inner_hop() {
        let mut headers = HeaderMap::new();
        headers.insert(
            "x-forwarded-for",
            "9.9.9.9, 198.51.100.4, 10.0.0.1".parse().unwrap(),
        );
        let peer: SocketAddr = "203.0.113.7:51000".parse().unwrap();
        assert_eq!(
            extract_client_ip(&headers, Some(peer), proxies(2)),
            "198.51.100.4"
        );
    }

    #[test]
    fn extract_client_ip_more_trusted_proxies_than_hops_uses_peer() {
        let mut headers = HeaderMap::new();
        headers.insert("x-forwarded-for", "10.0.0.1".parse().unwrap());
        let peer: SocketAddr = "203.0.113.7:51000".parse().unwrap();
        assert_eq!(
            extract_client_ip(&headers, Some(peer), proxies(2)),
            "203.0.113.7"
        );
    }

    #[test]
    fn extract_client_ip_ignores_forwarded_headers_for_direct_peer() {
        let mut headers = HeaderMap::new();
        headers.insert("x-forwarded-for", "9.9.9.9".parse().unwrap());
        headers.insert("x-real-ip", "9.9.9.9".parse().unwrap());
        let peer: SocketAddr = "203.0.113.7:51000".parse().unwrap();
        assert_eq!(
            extract_client_ip(&headers, Some(peer), ForwardedTrust::Peer),
            "203.0.113.7"
        );
    }

    #[test]
    fn extract_client_ip_direct_peer_without_socket_is_unknown() {
        let mut headers = HeaderMap::new();
        headers.insert("x-forwarded-for", "9.9.9.9".parse().unwrap());
        assert_eq!(
            extract_client_ip(&headers, None, ForwardedTrust::Peer),
            "unknown"
        );
    }

    #[test]
    fn test_parse_repeated_query_param_repeated() {
        let query = "did=test&cids=a&cids=b&cids=c";
        let result = parse_repeated_query_param(Some(query), "cids");
        assert_eq!(result, vec!["a", "b", "c"]);
    }

    #[test]
    fn test_parse_repeated_query_param_comma_separated() {
        let query = "did=test&cids=a,b,c";
        let result = parse_repeated_query_param(Some(query), "cids");
        assert_eq!(result, vec!["a", "b", "c"]);
    }

    #[test]
    fn test_parse_repeated_query_param_mixed() {
        let query = "did=test&cids=a,b&cids=c";
        let result = parse_repeated_query_param(Some(query), "cids");
        assert_eq!(result, vec!["a", "b", "c"]);
    }

    #[test]
    fn test_parse_repeated_query_param_single() {
        let query = "did=test&cids=a";
        let result = parse_repeated_query_param(Some(query), "cids");
        assert_eq!(result, vec!["a"]);
    }

    #[test]
    fn test_parse_repeated_query_param_empty() {
        let query = "did=test";
        let result = parse_repeated_query_param(Some(query), "cids");
        assert!(result.is_empty());
    }

    #[test]
    fn test_parse_repeated_query_param_url_encoded() {
        let query = "did=test&cids=bafyreib%2Btest";
        let result = parse_repeated_query_param(Some(query), "cids");
        assert_eq!(result, vec!["bafyreib+test"]);
    }

    #[test]
    fn test_generate_token_code() {
        let code = generate_token_code();
        assert_eq!(code.len(), 11);
        assert!(code.contains('-'));

        let parts: Vec<&str> = code.split('-').collect();
        assert_eq!(parts.len(), 2);
        assert_eq!(parts[0].len(), 5);
        assert_eq!(parts[1].len(), 5);

        assert!(
            code.chars()
                .filter(|&c| c != '-')
                .all(|c| BASE32_ALPHABET.to_uppercase().contains(c))
        );
    }

    #[test]
    fn test_generate_token_code_is_uppercase() {
        (0..100).for_each(|_| {
            let code = generate_token_code();
            assert_eq!(code, code.to_uppercase(), "code must be uppercase: {code}");
        });
    }

    #[test]
    fn test_normalize_token_code_strips_hyphen_and_uppercases() {
        assert_eq!(normalize_token_code("k7m2p-q9rst"), "K7M2PQ9RST");
        assert_eq!(normalize_token_code("K7M2P-Q9RST"), "K7M2PQ9RST");
    }

    #[test]
    fn test_normalize_token_code_strips_whitespace() {
        assert_eq!(normalize_token_code("  k7m2p-q9rst \n"), "K7M2PQ9RST");
    }

    #[test]
    fn test_normalize_token_code_is_idempotent() {
        let once = normalize_token_code("k7m2p-q9rst");
        assert_eq!(normalize_token_code(&once), once);
    }

    #[test]
    fn test_generated_code_round_trips_through_normalize() {
        let code = generate_token_code();
        // A user re-typing the displayed code lowercased and without the hyphen
        // must normalize to the same canonical form as the code itself.
        let retyped = code.to_lowercase().replace('-', "");
        assert_eq!(normalize_token_code(&code), normalize_token_code(&retyped));
    }

    #[test]
    fn test_json_to_ipld_cid_link() {
        let json = serde_json::json!({
            "$link": "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"
        });
        let ipld = json_to_ipld(&json);
        match ipld {
            Ipld::Link(cid) => {
                assert_eq!(
                    cid.to_string(),
                    "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"
                );
            }
            _ => panic!("Expected Ipld::Link, got {:?}", ipld),
        }
    }

    #[test]
    fn test_json_to_ipld_blob_ref() {
        let json = serde_json::json!({
            "$type": "blob",
            "ref": {
                "$link": "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"
            },
            "mimeType": "image/jpeg",
            "size": 12345
        });
        let ipld = json_to_ipld(&json);
        match ipld {
            Ipld::Map(map) => {
                assert_eq!(map.get("$type"), Some(&Ipld::String("blob".to_string())));
                assert_eq!(
                    map.get("mimeType"),
                    Some(&Ipld::String("image/jpeg".to_string()))
                );
                assert_eq!(map.get("size"), Some(&Ipld::Integer(12345)));
                match map.get("ref") {
                    Some(Ipld::Link(cid)) => {
                        assert_eq!(
                            cid.to_string(),
                            "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"
                        );
                    }
                    _ => panic!("Expected Ipld::Link in ref field, got {:?}", map.get("ref")),
                }
            }
            _ => panic!("Expected Ipld::Map, got {:?}", ipld),
        }
    }

    #[test]
    fn test_json_to_ipld_nested_blob_refs_serializes_correctly() {
        let record = serde_json::json!({
            "$type": "app.bsky.feed.post",
            "text": "Hello world",
            "embed": {
                "$type": "app.bsky.embed.images",
                "images": [
                    {
                        "alt": "Test image",
                        "image": {
                            "$type": "blob",
                            "ref": {
                                "$link": "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"
                            },
                            "mimeType": "image/jpeg",
                            "size": 12345
                        }
                    }
                ]
            }
        });
        let ipld = json_to_ipld(&record);
        let cbor_bytes = serde_ipld_dagcbor::to_vec(&ipld).expect("CBOR serialization failed");
        assert!(!cbor_bytes.is_empty());
        let parsed: Ipld =
            serde_ipld_dagcbor::from_slice(&cbor_bytes).expect("CBOR deserialization failed");
        if let Ipld::Map(map) = &parsed
            && let Some(Ipld::Map(embed)) = map.get("embed")
            && let Some(Ipld::List(images)) = embed.get("images")
            && let Some(Ipld::Map(img)) = images.first()
            && let Some(Ipld::Map(blob)) = img.get("image")
            && let Some(Ipld::Link(cid)) = blob.get("ref")
        {
            assert_eq!(
                cid.to_string(),
                "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"
            );
            return;
        }
        panic!("Failed to find CID link in parsed CBOR");
    }

    #[test]
    fn test_json_to_ipld_bytes_simple() {
        let json = serde_json::json!({
            "$bytes": "aGVsbG8gd29ybGQ="
        });
        let ipld = json_to_ipld(&json);
        match ipld {
            Ipld::Bytes(bytes) => {
                assert_eq!(bytes, b"hello world");
            }
            _ => panic!("Expected Ipld::Bytes, got {:?}", ipld),
        }
    }

    #[test]
    fn test_json_to_ipld_bytes_empty() {
        let json = serde_json::json!({
            "$bytes": ""
        });
        let ipld = json_to_ipld(&json);
        match ipld {
            Ipld::Bytes(bytes) => {
                assert!(bytes.is_empty());
            }
            _ => panic!("Expected Ipld::Bytes, got {:?}", ipld),
        }
    }

    #[test]
    fn test_json_to_ipld_bytes_with_special_base64_chars() {
        let json = serde_json::json!({
            "$bytes": "ygoGIpnVb/HQTIZythM9t1iLHkoWY5OeeqlhD0JEEgqHedDSCxG8F1YfipZPMA3JzKG6ssWNzOmZ9iSSW0nDvmjJ5ldwwbgt"
        });
        let ipld = json_to_ipld(&json);
        match ipld {
            Ipld::Bytes(bytes) => {
                assert!(!bytes.is_empty());
            }
            _ => panic!("Expected Ipld::Bytes, got {:?}", ipld),
        }
    }

    #[test]
    fn test_json_to_ipld_bytes_unpadded() {
        let padded = json_to_ipld(&serde_json::json!({ "$bytes": "aGVsbG8=" }));
        let unpadded = json_to_ipld(&serde_json::json!({ "$bytes": "aGVsbG8" }));
        match (&padded, &unpadded) {
            (Ipld::Bytes(a), Ipld::Bytes(b)) => {
                assert_eq!(a, b"hello");
                assert_eq!(b, b"hello");
            }
            _ => panic!(
                "Expected Ipld::Bytes for both, got {:?} / {:?}",
                padded, unpadded
            ),
        }
    }

    #[test]
    fn test_json_to_ipld_bytes_produces_cbor_byte_string_not_map() {
        let json = serde_json::json!({"$bytes": "SGVsbG8="});
        let ipld = json_to_ipld(&json);
        let cbor = serde_ipld_dagcbor::to_vec(&ipld).expect("CBOR serialization failed");
        assert_eq!(
            cbor[0] & 0xE0,
            0x40,
            "expected CBOR byte string (major type 2), got major type {}",
            cbor[0] >> 5
        );
    }

    #[test]
    fn test_json_to_ipld_bytes_not_confused_with_extra_keys() {
        let json = serde_json::json!({
            "$bytes": "aGVsbG8=",
            "extra": "field"
        });
        let ipld = json_to_ipld(&json);
        match ipld {
            Ipld::Map(_) => {}
            _ => panic!(
                "Expected Ipld::Map for $bytes with extra keys, got {:?}",
                ipld
            ),
        }
    }

    #[test]
    fn test_json_to_ipld_bytes_nested_in_record() {
        let record = serde_json::json!({
            "$type": "app.opake.grant",
            "recipient": "did:plc:example",
            "wrappedKey": {
                "algo": "x25519-hkdf-a256kw",
                "ciphertext": {
                    "$bytes": "ygoGIpnVb/HQTIZythM9t1iLHkoWY5OeeqlhD0JEEgqHedDSCxG8F1YfipZPMA3JzKG6ssWNzOmZ9iSSW0nDvmjJ5ldwwbgt"
                }
            },
            "encryptedMetadata": {
                "ciphertext": { "$bytes": "aGVsbG8=" },
                "nonce": { "$bytes": "d29ybGQ=" }
            }
        });
        let ipld = json_to_ipld(&record);
        let cbor_bytes = serde_ipld_dagcbor::to_vec(&ipld).expect("CBOR serialization failed");
        let parsed: Ipld =
            serde_ipld_dagcbor::from_slice(&cbor_bytes).expect("CBOR deserialization failed");
        if let Ipld::Map(map) = &parsed
            && let Some(Ipld::Map(wrapped)) = map.get("wrappedKey")
            && let Some(Ipld::Bytes(ct)) = wrapped.get("ciphertext")
            && let Some(Ipld::Map(meta)) = map.get("encryptedMetadata")
            && let Some(Ipld::Bytes(meta_ct)) = meta.get("ciphertext")
            && let Some(Ipld::Bytes(nonce)) = meta.get("nonce")
        {
            assert!(!ct.is_empty());
            assert_eq!(meta_ct, b"hello");
            assert_eq!(nonce, b"world");
            return;
        }
        panic!("Failed to find Bytes in parsed CBOR: {:?}", parsed);
    }

    #[test]
    fn test_parse_env_bool_true_values() {
        unsafe { std::env::set_var("TEST_PARSE_ENV_BOOL_1", "true") };
        assert!(parse_env_bool("TEST_PARSE_ENV_BOOL_1"));
        unsafe { std::env::set_var("TEST_PARSE_ENV_BOOL_1", "1") };
        assert!(parse_env_bool("TEST_PARSE_ENV_BOOL_1"));
    }

    #[test]
    fn test_parse_env_bool_false_values() {
        unsafe { std::env::set_var("TEST_PARSE_ENV_BOOL_2", "false") };
        assert!(!parse_env_bool("TEST_PARSE_ENV_BOOL_2"));
        unsafe { std::env::set_var("TEST_PARSE_ENV_BOOL_2", "0") };
        assert!(!parse_env_bool("TEST_PARSE_ENV_BOOL_2"));
        unsafe { std::env::set_var("TEST_PARSE_ENV_BOOL_2", "yes") };
        assert!(!parse_env_bool("TEST_PARSE_ENV_BOOL_2"));
    }

    #[test]
    fn test_parse_env_bool_unset() {
        unsafe { std::env::remove_var("TEST_PARSE_ENV_BOOL_UNSET_KEY") };
        assert!(!parse_env_bool("TEST_PARSE_ENV_BOOL_UNSET_KEY"));
    }

    #[test]
    fn test_build_full_url_adds_xrpc_prefix_for_atproto_paths() {
        unsafe { std::env::set_var("PDS_HOSTNAME", "example.com") };
        tranquil_config::ensure_test_defaults();
        assert_eq!(
            build_full_url("/com.atproto.server.getSession"),
            "https://example.com/xrpc/com.atproto.server.getSession"
        );
        assert_eq!(
            build_full_url("/app.bsky.feed.getTimeline"),
            match cfg!(feature = "bsky-support") {
                true => "https://example.com/xrpc/app.bsky.feed.getTimeline",
                false => "https://example.com/app.bsky.feed.getTimeline",
            }
        );
        assert_eq!(
            build_full_url("/_health"),
            "https://example.com/xrpc/_health"
        );
        assert_eq!(
            build_full_url("/xrpc/com.atproto.server.getSession"),
            "https://example.com/xrpc/com.atproto.server.getSession"
        );
        assert_eq!(
            build_full_url("/oauth/token"),
            "https://example.com/oauth/token"
        );
    }
}
