//! RFC 4648 base32 codec, no-padding form, lenient on input.
//!
//! The encoder produces canonical no-padding RFC 4648 output (uppercase ASCII).
//! The decoder is lenient: case-insensitive, optional `=` padding, whitespace
//! anywhere is stripped before decoding.
//!
//! Steam Guard's authenticator uses a different (de-ambiguated) alphabet —
//! see `crate::item_types::totp::STEAM_ALPHABET`. That codec is intentionally
//! NOT routed through this module.

use crate::error::{RelicarioError, Result};

const ALPHA: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

/// RFC 4648 base32 encoder, no-padding form. Output is uppercase ASCII.
pub fn encode_rfc4648(bytes: &[u8]) -> String {
    let mut out = String::new();
    let mut buffer: u32 = 0;
    let mut bits: u32 = 0;
    for &b in bytes {
        buffer = (buffer << 8) | (b as u32);
        bits += 8;
        while bits >= 5 {
            let idx = ((buffer >> (bits - 5)) & 0x1f) as usize;
            out.push(ALPHA[idx] as char);
            bits -= 5;
        }
    }
    if bits > 0 {
        let idx = ((buffer << (5 - bits)) & 0x1f) as usize;
        out.push(ALPHA[idx] as char);
    }
    out
}

/// RFC 4648 base32 decoder, lenient on input.
///
/// Accepts upper- or lower-case letters, optional `=` padding, and whitespace
/// anywhere. Trailing bits less than a full byte are silently discarded
/// (canonical RFC 4648 decode).
pub fn decode_rfc4648_lenient(s: &str) -> Result<Vec<u8>> {
    let cleaned: String = s
        .chars()
        .filter(|c| !c.is_whitespace())
        .collect::<String>()
        .to_ascii_uppercase();
    let trimmed = cleaned.trim_end_matches('=');
    let mut out: Vec<u8> = Vec::with_capacity(trimmed.len() * 5 / 8);
    let mut buffer: u32 = 0;
    let mut bits: u32 = 0;
    for ch in trimmed.bytes() {
        let idx = ALPHA.iter().position(|&a| a == ch).ok_or_else(|| {
            RelicarioError::InvalidBase32(format!("non-alphabet character {:?}", ch as char))
        })?;
        buffer = (buffer << 5) | (idx as u32);
        bits += 5;
        if bits >= 8 {
            bits -= 8;
            out.push(((buffer >> bits) & 0xff) as u8);
        }
    }
    Ok(out)
}

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

    #[test]
    fn encode_rfc4648_matches_rfc_test_vectors() {
        // RFC 4648 §10 test vectors, no-padding form.
        assert_eq!(encode_rfc4648(b""), "");
        assert_eq!(encode_rfc4648(b"f"), "MY");
        assert_eq!(encode_rfc4648(b"fo"), "MZXQ");
        assert_eq!(encode_rfc4648(b"foo"), "MZXW6");
        assert_eq!(encode_rfc4648(b"foob"), "MZXW6YQ");
        assert_eq!(encode_rfc4648(b"fooba"), "MZXW6YTB");
        assert_eq!(encode_rfc4648(b"foobar"), "MZXW6YTBOI");
    }

    #[test]
    fn decode_rfc4648_lenient_inverts_encoder_on_known_vectors() {
        let cases: &[(&str, &[u8])] = &[
            ("", b""),
            ("MY", b"f"),
            ("MZXQ", b"fo"),
            ("MZXW6", b"foo"),
            ("MZXW6YQ", b"foob"),
            ("MZXW6YTB", b"fooba"),
            ("MZXW6YTBOI", b"foobar"),
        ];
        for (s, want) in cases {
            assert_eq!(&decode_rfc4648_lenient(s).unwrap()[..], *want);
        }
    }

    #[test]
    fn decode_rfc4648_lenient_accepts_lowercase_and_mixed_case() {
        assert_eq!(decode_rfc4648_lenient("mzxw6").unwrap(), b"foo");
        assert_eq!(decode_rfc4648_lenient("MzXw6yTbOi").unwrap(), b"foobar");
    }

    #[test]
    fn decode_rfc4648_lenient_strips_optional_padding() {
        assert_eq!(decode_rfc4648_lenient("MY======").unwrap(), b"f");
        assert_eq!(decode_rfc4648_lenient("MZXW6===").unwrap(), b"foo");
        assert_eq!(decode_rfc4648_lenient("MZXW6YTBOI======").unwrap(), b"foobar");
    }

    #[test]
    fn decode_rfc4648_lenient_strips_whitespace_anywhere() {
        assert_eq!(decode_rfc4648_lenient(" MZXW 6YTB OI ").unwrap(), b"foobar");
        assert_eq!(decode_rfc4648_lenient("MZXW\n6YTB\tOI").unwrap(), b"foobar");
    }

    #[test]
    fn decode_rfc4648_lenient_rejects_non_alphabet_chars() {
        assert!(matches!(
            decode_rfc4648_lenient("MY1"),
            Err(RelicarioError::InvalidBase32(_))
        ));
        assert!(decode_rfc4648_lenient("???").is_err());
        assert!(decode_rfc4648_lenient("MZ!XW").is_err());
    }

    #[test]
    fn encode_decode_round_trips_arbitrary_bytes() {
        let bytes: Vec<u8> = (0u8..=255).collect();
        let encoded = encode_rfc4648(&bytes);
        assert_eq!(decode_rfc4648_lenient(&encoded).unwrap(), bytes);
    }
}
