//! Equivalence tests between `base64` crate and `base64ct`.

#![cfg(feature = "std")]
// TODO(tarcieri): fix `base64` crate deprecations
// warning: use of deprecated function `base64::encode`: Use Engine::encode
#![allow(deprecated)]

use base64ct::{Base64 as Base64ct, Base64Unpadded as Base64UnpaddedCt, Encoding};
use proptest::{prelude::*, string::*};

/// Incremental Base64 decoder.
type Decoder<'a> = base64ct::Decoder<'a, Base64ct>;

/// Incremental Base64 encoder.
type Encoder<'a> = base64ct::Encoder<'a, Base64ct>;

proptest! {
    /// Ensure `base64ct` decodes data encoded by `base64` ref crate
    #[test]
    fn decode_equiv(bytes in bytes_regex(".{0,256}").unwrap()) {
        let encoded = base64::encode(&bytes);
        let decoded = Base64ct::decode_vec(&encoded);
        prop_assert_eq!(Ok(bytes), decoded);
    }

    /// Ensure that `base64ct`'s incremental decoder is able to decode randomly
    /// generated inputs encoded by the `base64` ref crate
    #[test]
    fn decode_incremental(bytes in bytes_regex(".{1,256}").unwrap(), chunk_size in 1..256usize) {
        let encoded = base64::encode(&bytes);
        let chunk_size = match chunk_size % bytes.len() {
            0 => 1,
            n => n
        };

        let mut buffer = [0u8; 384];
        let mut decoder = Decoder::new(encoded.as_bytes()).unwrap();
        let mut remaining_len = decoder.remaining_len();

        for chunk in bytes.chunks(chunk_size) {
            prop_assert!(!decoder.is_finished());

            let decoded = decoder.decode(&mut buffer[..chunk.len()]);
            prop_assert_eq!(Ok(chunk), decoded);

            remaining_len -= decoded.unwrap().len();
            prop_assert_eq!(remaining_len, decoder.remaining_len());
        }

        prop_assert!(decoder.is_finished());
        prop_assert_eq!(decoder.remaining_len(), 0);
    }

    #[test]
    fn decode_incremental_wrapped(
        bytes in bytes_regex(".{1,256}").unwrap(),
        line_width in 4..128usize,
        chunk_size in 1..256usize
    ) {
        for line_ending in ["\r", "\n", "\r\n"] {
            let encoded = base64::encode(&bytes);

            let mut encoded_wrapped = Vec::new();
            let mut lines = encoded.as_bytes().chunks_exact(line_width);

            for line in &mut lines {
                encoded_wrapped.extend_from_slice(line);
                encoded_wrapped.extend_from_slice(line_ending.as_bytes());
            }

            let last = lines.remainder();

            if last.is_empty() {
                encoded_wrapped.truncate(encoded_wrapped.len() - line_ending.len());
            } else {
                encoded_wrapped.extend_from_slice(last);
            }

            let chunk_size = match chunk_size % bytes.len() {
                0 => 1,
                n => n
            };

            let mut buffer = [0u8; 384];
            let mut decoder = Decoder::new_wrapped(&encoded_wrapped, line_width).unwrap();
            let mut remaining_len = decoder.remaining_len();

            for chunk in bytes.chunks(chunk_size) {
                prop_assert!(!decoder.is_finished());

                let decoded = decoder.decode(&mut buffer[..chunk.len()]);
                prop_assert_eq!(Ok(chunk), decoded);

                remaining_len -= decoded.unwrap().len();
                prop_assert_eq!(remaining_len, decoder.remaining_len());
            }

            prop_assert!(decoder.is_finished());
            prop_assert_eq!(decoder.remaining_len(), 0);
        }
    }

    /// Ensure `base64ct` and `base64` ref crate decode randomly generated
    /// inputs equivalently.
    ///
    /// Inputs are selected to be valid characters in the standard Base64
    /// padded alphabet, but are not necessarily valid Base64.
    #[test]
    fn decode_random(base64ish in string_regex("[A-Za-z0-9+/]{0,256}").unwrap()) {
        let base64ish_padded = match base64ish.len() % 4 {
            0 => base64ish,
            n => {
                let padding_len = 4 - n;
                base64ish + &"=".repeat(padding_len)
            }
        };

        let decoded_ct = Base64ct::decode_vec(&base64ish_padded).ok();
        let decoded_ref = base64::decode(&base64ish_padded).ok();
        prop_assert_eq!(decoded_ct, decoded_ref);
    }

    /// Ensure `base64ct` and the `base64` ref crate encode randomly generated
    /// inputs equivalently.
    #[test]
    fn encode_equiv(bytes in bytes_regex(".{0,256}").unwrap()) {
        let encoded_ct = Base64ct::encode_string(&bytes);
        let encoded_ref = base64::encode(&bytes);
        prop_assert_eq!(encoded_ct, encoded_ref);
    }

    /// Ensure that `base64ct`'s incremental encoder is able to encode randomly
    /// generated inputs which match what's encoded by the `base64` ref crate
    #[test]
    fn encode_incremental(bytes in bytes_regex(".{1,256}").unwrap(), chunk_size in 1..256usize) {
        let expected = base64::encode(&bytes);
        let chunk_size = match chunk_size % bytes.len() {
            0 => 1,
            n => n
        };

        let mut buffer = [0u8; 1024];
        let mut encoder = Encoder::new(&mut buffer).unwrap();

        for chunk in bytes.chunks(chunk_size) {
            encoder.encode(chunk).unwrap();
        }

        prop_assert_eq!(expected, encoder.finish().unwrap());
    }

    /// Make sure that base64ct and base64 both decode the same values
    /// when expecting padded inputs, and produce the same outputs for those values.
    #[test]
    fn decode_arbitrary_padded(string in string_regex("[a-zA-Z0-9/+=?]{0,256}").unwrap()) {
        let actual = Base64ct::decode_vec(&string);
        let expected = base64::decode( &string);
        assert_eq!(actual.ok(), expected.ok());
    }

    /// Make sure that base64ct and base64 both decode the same values
    /// when expecting unpadded inputs, and produce the same outputs for those values.
    #[test]
    fn decode_arbitrary_unpadded(string in string_regex("[a-zA-Z0-9/+=?]{0,256}").unwrap()) {
        use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
        let actual = Base64UnpaddedCt::decode_vec(&string);
        let expected = STANDARD_NO_PAD.decode(&string);
        assert_eq!(actual.ok(), expected.ok());
    }
}
