use super::*;

struct DispatchFallbackAlphabet;

impl Alphabet for DispatchFallbackAlphabet {
    const ENCODE: [u8; 64] = *b"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    fn decode(byte: u8) -> Option<u8> {
        decode_alphabet_byte(byte, &Self::ENCODE)
    }
}

fn fill_pattern(output: &mut [u8], seed: usize) {
    for (index, byte) in output.iter_mut().enumerate() {
        let value = (index * 73 + seed * 19) % 256;
        *byte = u8::try_from(value).unwrap();
    }
}

fn assert_encode_in_place_backend_matches_scalar<A, const PAD: bool>(input: &[u8])
where
    A: Alphabet,
{
    let engine = Engine::<A, PAD>::new();
    let mut dispatched = [0x55; 256];
    let mut scalar = [0xaa; 256];

    dispatched[..input.len()].copy_from_slice(input);
    scalar[..input.len()].copy_from_slice(input);

    let dispatched_result = engine
        .encode_in_place(&mut dispatched, input.len())
        .map(|encoded| encoded.len());
    let scalar_result = scalar_encode_in_place::encode_in_place::<A, PAD>(&mut scalar, input.len());

    assert_eq!(dispatched_result, scalar_result);
    if let Ok(written) = dispatched_result {
        assert_eq!(&dispatched[..written], &scalar[..written]);
    }
}

fn assert_standard_family_encode_surface_matches_scalar<A, const PAD: bool>(input: &[u8])
where
    A: Alphabet,
{
    let engine = Engine::<A, PAD>::new();
    let mut encoded = [0x55; 512];
    let mut clear_tail = [0x66; 512];
    let mut scalar = [0xaa; 512];

    let encoded_len = engine.encode_slice(input, &mut encoded).unwrap();
    let clear_tail_len = engine
        .encode_slice_clear_tail(input, &mut clear_tail)
        .unwrap();
    let scalar_len = scalar::scalar_reference_encode_slice::<A, PAD>(input, &mut scalar).unwrap();

    assert_eq!(encoded_len, scalar_len);
    assert_eq!(clear_tail_len, scalar_len);
    assert_eq!(&encoded[..encoded_len], &scalar[..scalar_len]);
    assert_eq!(&clear_tail[..clear_tail_len], &scalar[..scalar_len]);
    assert!(clear_tail[clear_tail_len..].iter().all(|byte| *byte == 0));

    let stack_buffer = engine.encode_buffer::<512>(input).unwrap();
    assert_eq!(stack_buffer.as_bytes(), &scalar[..scalar_len]);

    #[cfg(feature = "alloc")]
    {
        let encoded_vec = engine.encode_vec(input).unwrap();
        let encoded_vec_infallible = engine.encode_vec_infallible(input);
        let encoded_string = engine.encode_string(input).unwrap();
        let encoded_string_infallible = engine.encode_string_infallible(input);

        assert_eq!(encoded_vec, &scalar[..scalar_len]);
        assert_eq!(encoded_vec_infallible, &scalar[..scalar_len]);
        assert_eq!(encoded_string.as_bytes(), &scalar[..scalar_len]);
        assert_eq!(encoded_string_infallible.as_bytes(), &scalar[..scalar_len]);
    }
}

#[test]
fn standard_family_encode_surfaces_cover_tails_and_padding() {
    let mut input = [0; 193];

    for input_len in 0..=input.len() {
        fill_pattern(&mut input[..input_len], input_len);
        let input = &input[..input_len];

        assert_standard_family_encode_surface_matches_scalar::<Standard, true>(input);
        assert_standard_family_encode_surface_matches_scalar::<Standard, false>(input);
        assert_standard_family_encode_surface_matches_scalar::<UrlSafe, true>(input);
        assert_standard_family_encode_surface_matches_scalar::<UrlSafe, false>(input);
    }
}

#[test]
fn encode_in_place_backend_matches_scalar_reference() {
    let mut input = [0; 128];

    for input_len in 0..=input.len() {
        fill_pattern(&mut input[..input_len], input_len);
        let input = &input[..input_len];

        assert_encode_in_place_backend_matches_scalar::<Standard, true>(input);
        assert_encode_in_place_backend_matches_scalar::<Standard, false>(input);
        assert_encode_in_place_backend_matches_scalar::<UrlSafe, true>(input);
        assert_encode_in_place_backend_matches_scalar::<UrlSafe, false>(input);
        assert_encode_in_place_backend_matches_scalar::<DispatchFallbackAlphabet, true>(input);
        assert_encode_in_place_backend_matches_scalar::<DispatchFallbackAlphabet, false>(input);
        assert_encode_in_place_backend_matches_scalar::<Bcrypt, false>(input);
        assert_encode_in_place_backend_matches_scalar::<Crypt, false>(input);
    }
}

#[test]
fn encode_simd_admission_rejects_non_standard_alphabet_families() {
    #[cfg(all(
        feature = "simd",
        feature = "std",
        any(target_arch = "x86", target_arch = "x86_64")
    ))]
    for supports_alphabet in [
        simd::avx512_supports_alphabet::<DispatchFallbackAlphabet>,
        simd::avx512_supports_alphabet::<Bcrypt>,
        simd::avx512_supports_alphabet::<Crypt>,
        simd::avx2_supports_alphabet::<DispatchFallbackAlphabet>,
        simd::avx2_supports_alphabet::<Bcrypt>,
        simd::avx2_supports_alphabet::<Crypt>,
        simd::ssse3_sse41_supports_alphabet::<DispatchFallbackAlphabet>,
        simd::ssse3_sse41_supports_alphabet::<Bcrypt>,
        simd::ssse3_sse41_supports_alphabet::<Crypt>,
    ] {
        assert!(!supports_alphabet());
    }

    #[cfg(all(
        feature = "simd",
        feature = "std",
        target_arch = "aarch64",
        target_endian = "little"
    ))]
    for supports_alphabet in [
        simd::neon_supports_alphabet::<DispatchFallbackAlphabet>,
        simd::neon_supports_alphabet::<Bcrypt>,
        simd::neon_supports_alphabet::<Crypt>,
    ] {
        assert!(!supports_alphabet());
    }
}
