// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
#![allow(clippy::naive_bytecount)]

use rand::{
    Rng, RngExt as _,
    distr::{Distribution, Uniform},
    rng,
};

/// Samples alphanumeric characters `[A-Za-z0-9]` including newline `\n`
///
/// # Examples
///
/// ```rust,ignore
/// use rand::{Rng, rng};
///
/// let vec = rng()
///     .sample_iter(AlphanumericNewline)
///     .take(10)
///     .collect::<Vec<u8>>();
/// println!("Random chars: {}", String::from_utf8(vec).unwrap());
/// ```
#[derive(Clone, Copy, Debug)]
pub struct AlphanumericNewline;

impl AlphanumericNewline {
    /// The charset to act upon
    const CHARSET: &'static [u8] =
        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\n";

    /// Generate a random byte from [`Self::CHARSET`] and return it as `u8`.
    ///
    /// # Arguments
    ///
    /// * `rng`: A [`rand::Rng`]
    ///
    /// returns: u8
    fn random<R>(rng: &mut R) -> u8
    where
        R: Rng + ?Sized,
    {
        let idx = rng.random_range(0..Self::CHARSET.len());
        Self::CHARSET[idx]
    }
}

impl Distribution<u8> for AlphanumericNewline {
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> u8 {
        Self::random(rng)
    }
}

/// Generate a random string from a [`Distribution`]
///
/// # Examples
///
/// ```rust,ignore
/// use crate::common::random::{AlphanumericNewline, RandomizedString};
/// use rand::distributions::Alphanumeric;
///
/// // generates a 100 byte string with characters from AlphanumericNewline
/// let random_string = RandomizedString::generate(AlphanumericNewline, 100);
/// assert_eq!(100, random_string.len());
///
/// // generates a 100 byte string with 10 newline characters not ending with a newline
/// let string = RandomizedString::generate_with_delimiter(Alphanumeric, b'\n', 10, false, 100);
/// assert_eq!(100, random_string.len());
/// ```
pub struct RandomizedString;

impl RandomizedString {
    /// Generate a random string from the given [`Distribution`] with the given `length` in bytes.
    ///
    /// # Arguments
    ///
    /// * `dist`: A u8 [`Distribution`]
    /// * `length`: the length of the resulting string in bytes
    ///
    /// returns: String
    pub fn generate<D>(dist: D, length: usize) -> String
    where
        D: Distribution<u8>,
    {
        rng()
            .sample_iter(dist)
            .take(length)
            .map(|b| b as char)
            .collect()
    }

    /// Generate a random string from the [`Distribution`] with the given `length` in bytes. The
    /// function takes a `delimiter`, which is randomly distributed in the string, such that exactly
    /// `num_delimiter` amount of `delimiter`s occur. If `end_with_delimiter` is set, then the
    /// string ends with the delimiter, else the string does not end with the delimiter.
    ///
    /// # Arguments
    ///
    /// * `dist`: A `u8` [`Distribution`]
    /// * `delimiter`: A `u8` delimiter, which does not need to be included in the `Distribution`
    /// * `num_delimiter`: The number of `delimiter`s contained in the resulting string
    /// * `end_with_delimiter`: If the string shall end with the given delimiter
    /// * `length`: the length of the resulting string in bytes
    ///
    /// returns: String
    ///
    /// # Examples
    ///
    /// ```rust,ignore
    /// use crate::common::random::{AlphanumericNewline, RandomizedString};
    ///
    /// // generates a 100 byte string with 10 '\0' byte characters not ending with a '\0' byte
    /// let string = RandomizedString::generate_with_delimiter(AlphanumericNewline, 0, 10, false, 100);
    /// assert_eq!(100, random_string.len());
    /// assert_eq!(
    ///     10,
    ///     random_string.as_bytes().iter().filter(|p| **p == 0).count()
    /// );
    /// assert!(!random_string.as_bytes().ends_with(&[0]));
    /// ```
    pub fn generate_with_delimiter<D>(
        dist: D,
        delimiter: u8,
        num_delimiter: usize,
        end_with_delimiter: bool,
        length: usize,
    ) -> String
    where
        D: Distribution<u8>,
    {
        if length == 0 {
            return String::new();
        } else if length == 1 {
            return if num_delimiter > 0 {
                String::from(delimiter as char)
            } else {
                String::from(rng().sample(&dist) as char)
            };
        }

        let samples = length - 1;
        let mut result: Vec<u8> = rng().sample_iter(&dist).take(samples).collect();

        if num_delimiter == 0 {
            result.push(rng().sample(&dist));
            return String::from_utf8(result).unwrap();
        }

        let num_delimiter = if end_with_delimiter {
            num_delimiter - 1
        } else {
            num_delimiter
        };

        // it's safe to unwrap because samples is always > 0, thus low < high
        let between = Uniform::new(0, samples).unwrap();
        for _ in 0..num_delimiter {
            let mut pos = between.sample(&mut rng());
            let turn = pos;
            while result[pos] == delimiter {
                pos += 1;
                if pos >= samples {
                    pos = 0;
                }
                if pos == turn {
                    break;
                }
            }
            result[pos] = delimiter;
        }

        if end_with_delimiter {
            result.push(delimiter);
        } else {
            result.push(rng().sample(&dist));
        }

        String::from_utf8(result).unwrap()
    }
}

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

    #[test]
    fn test_random_string_generate() {
        let random_string = RandomizedString::generate(AlphanumericNewline, 0);
        assert_eq!(0, random_string.len());

        let random_string = RandomizedString::generate(AlphanumericNewline, 1);
        assert_eq!(1, random_string.len());

        let random_string = RandomizedString::generate(AlphanumericNewline, 100);
        assert_eq!(100, random_string.len());
    }

    #[test]
    fn test_random_string_generate_with_delimiter_when_length_is_zero() {
        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 0, false, 0);
        assert_eq!(0, random_string.len());
    }

    #[test]
    fn test_random_string_generate_with_delimiter_when_num_delimiter_is_greater_than_length() {
        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 2, false, 1);
        assert_eq!(1, random_string.len());
        assert!(random_string.as_bytes().contains(&0));
        assert!(random_string.as_bytes().ends_with(&[0]));
    }

    #[test]
    #[allow(clippy::cognitive_complexity)] // Ignore clippy lint of too long function sign
    fn test_random_string_generate_with_delimiter_should_end_with_delimiter() {
        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 1, true, 1);
        assert_eq!(1, random_string.len());
        assert_eq!(
            1,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(random_string.as_bytes().ends_with(&[0]));

        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 1, false, 1);
        assert_eq!(1, random_string.len());
        assert_eq!(
            1,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(random_string.as_bytes().ends_with(&[0]));

        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 1, true, 2);
        assert_eq!(2, random_string.len());
        assert_eq!(
            1,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(random_string.as_bytes().ends_with(&[0]));

        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 2, true, 2);
        assert_eq!(2, random_string.len());
        assert_eq!(
            2,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(random_string.as_bytes().ends_with(&[0]));

        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 1, true, 3);
        assert_eq!(3, random_string.len());
        assert_eq!(
            1,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(random_string.as_bytes().ends_with(&[0]));
    }

    #[test]
    #[allow(clippy::cognitive_complexity)] // Ignore clippy lint of too long function sign
    fn test_random_string_generate_with_delimiter_should_not_end_with_delimiter() {
        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 0, false, 1);
        assert_eq!(1, random_string.len());
        assert_eq!(
            0,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );

        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 0, true, 1);
        assert_eq!(1, random_string.len());
        assert_eq!(
            0,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );

        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 1, false, 2);
        assert_eq!(2, random_string.len());
        assert_eq!(
            1,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(!random_string.as_bytes().ends_with(&[0]));

        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 1, false, 3);
        assert_eq!(3, random_string.len());
        assert_eq!(
            1,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(!random_string.as_bytes().ends_with(&[0]));

        let random_string = RandomizedString::generate_with_delimiter(Alphanumeric, 0, 2, false, 3);
        assert_eq!(3, random_string.len());
        assert_eq!(
            2,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(!random_string.as_bytes().ends_with(&[0]));
    }

    #[test]
    fn test_generate_with_delimiter_with_greater_length() {
        let random_string =
            RandomizedString::generate_with_delimiter(Alphanumeric, 0, 100, false, 1000);
        assert_eq!(1000, random_string.len());
        assert_eq!(
            100,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(!random_string.as_bytes().ends_with(&[0]));

        let random_string =
            RandomizedString::generate_with_delimiter(Alphanumeric, 0, 100, true, 1000);
        assert_eq!(1000, random_string.len());
        assert_eq!(
            100,
            random_string.as_bytes().iter().filter(|p| **p == 0).count()
        );
        assert!(random_string.as_bytes().ends_with(&[0]));
    }

    /// Originally used to exclude an error within the `random` module. The two
    /// affected tests timed out on windows, but only in the ci. These tests are
    /// also the source for the concrete numbers. The timed out tests are
    /// `test_tail.rs::test_pipe_when_lines_option_given_input_size_has_multiple_size_of_buffer_size`
    /// `test_tail.rs::test_pipe_when_bytes_option_given_input_size_has_multiple_size_of_buffer_size`.
    #[test]
    fn test_generate_random_strings_when_length_is_around_critical_buffer_sizes() {
        let length = 8192 * 3;
        let random_string = RandomizedString::generate(AlphanumericNewline, length);
        assert_eq!(length, random_string.len());

        let length = 8192 * 3 + 1;
        let random_string =
            RandomizedString::generate_with_delimiter(Alphanumeric, b'\n', 100, true, length);
        assert_eq!(length, random_string.len());
        assert_eq!(
            100,
            random_string
                .as_bytes()
                .iter()
                .filter(|p| **p == b'\n')
                .count()
        );
        assert!(!random_string.as_bytes().ends_with(&[0]));
    }
}
