//! High-entropy opaque tokens for sessions and bot API access.
//!
//! `generate_token` draws 32 bytes from the OS CSPRNG, encodes them as
//! URL-safe base64 (no padding), and returns the raw string alongside its
//! SHA-256 hash. Storage holds only the hash; the raw value lives in the
//! cookie or `Authorization` header. Comparison goes through
//! `constant_time_eq` to keep timing side channels off the table.

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use rand::rngs::OsRng;
use rand::RngCore;
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;

pub const TOKEN_BYTES: usize = 32;
pub const HASH_BYTES: usize = 32;

pub fn generate_token() -> (String, [u8; HASH_BYTES]) {
    let mut raw = [0u8; TOKEN_BYTES];
    OsRng.fill_bytes(&mut raw);
    let encoded = URL_SAFE_NO_PAD.encode(raw);
    let hash = hash_token(&encoded);
    (encoded, hash)
}

pub fn hash_token(raw: &str) -> [u8; HASH_BYTES] {
    let mut hasher = Sha256::new();
    hasher.update(raw.as_bytes());
    hasher.finalize().into()
}

pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
    a.ct_eq(b).into()
}

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

    #[test]
    fn generates_unique_tokens() {
        let (a, _) = generate_token();
        let (b, _) = generate_token();
        assert_ne!(a, b);
        // 32 bytes base64url-no-pad → 43 chars.
        assert_eq!(a.len(), 43);
    }

    #[test]
    fn hash_matches_generated_pair() {
        let (raw, hash) = generate_token();
        assert_eq!(hash_token(&raw), hash);
    }

    #[test]
    fn hash_is_deterministic() {
        assert_eq!(hash_token("abc"), hash_token("abc"));
        assert_ne!(hash_token("abc"), hash_token("abd"));
    }

    #[test]
    fn constant_time_eq_compares_correctly() {
        assert!(constant_time_eq(b"abc", b"abc"));
        assert!(!constant_time_eq(b"abc", b"abd"));
        assert!(!constant_time_eq(b"abc", b"abcd"));
    }
}
