Skip to main content

chamber_password_gen/
lib.rs

1use color_eyre::Result;
2use color_eyre::eyre::eyre;
3use rand::seq::{IndexedRandom, SliceRandom};
4use rand::{Rng, rng};
5#[allow(clippy::struct_excessive_bools)]
6#[derive(Debug, Clone)]
7pub struct PasswordConfig {
8    pub length: usize,
9    pub include_uppercase: bool,
10    pub include_lowercase: bool,
11    pub include_digits: bool,
12    pub include_symbols: bool,
13    pub exclude_ambiguous: bool,
14}
15
16impl Default for PasswordConfig {
17    fn default() -> Self {
18        Self {
19            length: 16,
20            include_uppercase: true,
21            include_lowercase: true,
22            include_digits: true,
23            include_symbols: true,
24            exclude_ambiguous: true,
25        }
26    }
27}
28
29impl PasswordConfig {
30    #[must_use]
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    #[must_use]
36    pub fn with_length(mut self, length: usize) -> Self {
37        self.length = length.clamp(4, 128); // Reasonable bounds
38        self
39    }
40
41    #[must_use]
42    pub const fn with_uppercase(mut self, include: bool) -> Self {
43        self.include_uppercase = include;
44        self
45    }
46
47    #[must_use]
48    pub const fn with_lowercase(mut self, include: bool) -> Self {
49        self.include_lowercase = include;
50        self
51    }
52
53    #[must_use]
54    pub const fn with_digits(mut self, include: bool) -> Self {
55        self.include_digits = include;
56        self
57    }
58
59    #[must_use]
60    pub const fn with_symbols(mut self, include: bool) -> Self {
61        self.include_symbols = include;
62        self
63    }
64
65    #[must_use]
66    pub const fn with_exclude_ambiguous(mut self, exclude: bool) -> Self {
67        self.exclude_ambiguous = exclude;
68        self
69    }
70
71    /// Validate that at least one character set is enabled
72    #[must_use]
73    pub const fn is_valid(&self) -> bool {
74        self.include_uppercase || self.include_lowercase || self.include_digits || self.include_symbols
75    }
76
77    /// Get the character sets based on configuration
78    fn get_character_sets(&self) -> Vec<&'static str> {
79        let mut sets = Vec::new();
80
81        if self.include_lowercase {
82            if self.exclude_ambiguous {
83                sets.push("abcdefghijkmnopqrstuvwxyz"); // Excludes 'l'
84            } else {
85                sets.push("abcdefghijklmnopqrstuvwxyz");
86            }
87        }
88
89        if self.include_uppercase {
90            if self.exclude_ambiguous {
91                sets.push("ABCDEFGHJKLMNPQRSTUVWXYZ"); // Excludes 'I', 'O'
92            } else {
93                sets.push("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
94            }
95        }
96
97        if self.include_digits {
98            if self.exclude_ambiguous {
99                sets.push("23456789"); // Excludes '0', '1'
100            } else {
101                sets.push("0123456789");
102            }
103        }
104
105        if self.include_symbols {
106            if self.exclude_ambiguous {
107                sets.push("!@#$%^&*+-=?"); // Excludes similar looking symbols
108            } else {
109                sets.push("!@#$%^&*()_+-=[]{}|;:,.<>?");
110            }
111        }
112
113        sets
114    }
115
116    /// Generates a random password based on the character sets and length defined in the struct.
117    ///
118    /// # Returns
119    /// - `Ok(String)` containing the generated password as per the specified requirements.
120    /// - `Err(anyhow::Error)` if the password generation fails due to invalid configuration.
121    ///
122    /// The function performs the following steps:
123    /// 1. Validates that at least one character set is enabled using `self.is_valid()`.
124    ///    - If no character set is enabled, an error is returned indicating this condition.
125    /// 2. Computes the available character sets as a vector of strings by calling `self.get_character_sets()`.
126    /// 3. Collects all unique characters from these sets into a single collection.
127    ///    - If the resulting collection is empty, an error is returned indicating no valid characters are available.
128    /// 4. Ensures that the generated password contains at least one character from each enabled set.
129    /// 5. Randomly fills the rest of the password until the desired length is reached.
130    /// 6. The generated password is shuffled to avoid predictable patterns.
131    ///
132    /// # Errors
133    /// - Returns an error if:
134    ///   - No character sets are enabled (`"At least one character set must be enabled"`).
135    ///   - No valid characters are available (`"No valid characters available"`).
136    ///
137    /// # Notes
138    /// - The `self.is_valid()` method is expected to verify whether at least one character set is enabled.
139    /// - The `self.get_character_sets()` method should return a collection of strings, each containing
140    ///   a group of valid characters (e.g., lowercase, uppercase, digits, symbols).
141    /// - The password length is determined by `self.length`, which should already be validated to be a valid size.
142    /// - This method uses randomness; ensure you have a valid random number generator (`rng()`).
143    pub fn generate(&self) -> Result<String> {
144        if !self.is_valid() {
145            return Err(eyre!("At least one character set must be enabled"));
146        }
147
148        let character_sets = self.get_character_sets();
149        let all_chars: String = character_sets.join("");
150        let all_chars: Vec<char> = all_chars.chars().collect();
151
152        if all_chars.is_empty() {
153            return Err(eyre!("No valid characters available"));
154        }
155
156        let mut rng = rng();
157        let mut password = Vec::with_capacity(self.length);
158
159        // Ensure at least one character from each enabled set
160        for set in &character_sets {
161            let chars: Vec<char> = set.chars().collect();
162            if let Some(&ch) = chars.choose(&mut rng) {
163                password.push(ch);
164            }
165        }
166
167        // Fill the rest randomly
168        while password.len() < self.length {
169            if let Some(&ch) = all_chars.choose(&mut rng) {
170                password.push(ch);
171            }
172        }
173
174        // Shuffle the password to avoid predictable patterns
175        password.shuffle(&mut rng);
176
177        Ok(password.into_iter().collect())
178    }
179}
180
181/// Generates a simple password of the specified length.
182///
183/// This function utilizes the `PasswordConfig` structure to generate a password
184/// with certain constraints set:
185/// - The length of the password is determined by the `length` parameter.
186/// - The password will not include symbols, only alphanumeric characters.
187///
188/// # Arguments
189///
190/// * `length` - A `usize` value specifying the desired length of the password.
191///
192/// # Returns
193///
194/// Returns an `anyhow::Result<String>`:
195/// - `Ok(String)` containing the generated password if successful.
196/// - `Err(anyhow::Error)` if an error occurs during password generation.
197///
198/// # Errors
199///
200/// This function may return an error if the `PasswordConfig` fails to generate
201/// a password due to invalid parameters or runtime issues.
202///
203/// Note: Ensure the `length` parameter is a positive value within acceptable limits
204/// supported by the password generator, as extremely large lengths may cause failures.
205pub fn generate_simple_password(length: usize) -> Result<String> {
206    PasswordConfig::new().with_length(length).with_symbols(false).generate()
207}
208
209/// Generates a complex password based on the specified length.
210///
211/// The function utilizes the `PasswordConfig` struct to create a password with specific parameters.
212/// It ensures the password generation process includes all necessary configurations to create a robust and secure password.
213///
214/// # Parameters
215/// - `length`: A `usize` value representing the desired length of the generated password.
216///
217/// # Returns
218/// - Returns an `anyhow::Result<String>`:
219///   - On success: A `String` containing the generated password.
220///   - On failure: An error wrapped in an `anyhow::Result` indicating what went wrong during generation.
221///
222/// # Behavior
223/// - The generated password includes alphanumeric characters as well as symbols for added complexity.
224/// - The function explicitly permits the inclusion of ambiguous characters (e.g., characters that may be difficult to distinguish visually).
225///
226/// # Errors
227/// - The function returns an error if there is a failure in the password generation process.
228///   This may occur due to issues with underlying configurations or constraints.
229///
230/// # Notes
231/// - Ensure to validate the `length` parameter before calling the function, as extremely large or small lengths
232///   may not be suitable based on the use case.
233///
234/// # Dependencies
235/// - The function relies on the `PasswordConfig` struct and its associated methods, which are assumed to be properly defined elsewhere in the codebase.
236pub fn generate_complex_password(length: usize) -> Result<String> {
237    PasswordConfig::new()
238        .with_length(length)
239        .with_exclude_ambiguous(false)
240        .generate()
241}
242
243/// Generates a memorable yet relatively secure password by combining randomly chosen
244/// syllables, numbers, and separators.
245///
246/// # Details
247/// - The password consists of two "words," each made up of 3–4 randomly selected syllables
248///   from a predefined list of simple syllables for enhanced readability.
249/// - The first syllable of the first word is capitalized to make the password more
250///   distinguishable.
251/// - A numeric separator is added between the two "words."
252/// - The password is appended with two random digits at the end to increase variability
253///   and entropy.
254///
255/// # Returns
256/// A `String` containing the generated password.
257///
258/// # Behavior
259/// - Each execution produces a different password due to randomization.
260/// - Example format: `BaCU4raze19`
261///
262/// # Attributes
263/// - **#[`must_use`]:** This function returns a value that should be used; failing to
264///   use the returned password may indicate a mistake.
265///
266/// # Panics
267/// - The function uses `expect` to ensure that digit selection from the allowed range
268///   (0–9) succeeds. Panics will only occur if the internal logic of random digit
269///   generation fails (highly unlikely).
270///
271/// # Notes
272/// - Designed for use cases requiring user-friendly yet secure passphrases.
273/// - Consider using additional validation if stronger security is required.
274#[must_use]
275pub fn generate_memorable_password() -> String {
276    let mut rng = rng();
277
278    // Simple syllables for readability
279    let syllables = [
280        "ba", "be", "bi", "bo", "bu", "ca", "ce", "ci", "co", "cu", "da", "de", "di", "do", "du", "fa", "fe", "fi",
281        "fo", "fu", "ga", "ge", "gi", "go", "gu", "ha", "he", "hi", "ho", "hu", "ja", "je", "ji", "jo", "ju", "ka",
282        "ke", "ki", "ko", "ku", "la", "le", "li", "lo", "lu", "ma", "me", "mi", "mo", "mu", "na", "ne", "ni", "no",
283        "nu", "pa", "pe", "pi", "po", "pu", "ra", "re", "ri", "ro", "ru", "sa", "se", "si", "so", "su", "ta", "te",
284        "ti", "to", "tu", "va", "ve", "vi", "vo", "vu", "wa", "we", "wi", "wo", "wu", "ya", "ye", "yi", "yo", "yu",
285        "za", "ze", "zi", "zo", "zu",
286    ];
287
288    let mut password = String::new();
289
290    // Generate 3-4 syllable words
291    for i in 0..2 {
292        let word_length = rng.random_range(3..=4);
293        for j in 0..word_length {
294            if let Some(&syllable) = syllables.choose(&mut rng) {
295                if j == 0 && i == 0 {
296                    // Capitalize first syllable of first word
297                    password.push_str(&syllable.to_uppercase());
298                } else {
299                    password.push_str(syllable);
300                }
301            }
302        }
303
304        // Add separator
305        if i == 0 {
306            #[allow(clippy::expect_used)]
307            password.push(
308                rng.random_range(0..=9)
309                    .to_string()
310                    .chars()
311                    .next()
312                    .expect("No ASCII digits"),
313            );
314        }
315    }
316
317    #[allow(clippy::expect_used)]
318    // Add some digits at the end
319    for _ in 0..2 {
320        password.push(
321            rng.random_range(0..=9)
322                .to_string()
323                .chars()
324                .next()
325                .expect("No ASCII digits"),
326        );
327    }
328
329    password
330}
331
332#[cfg(test)]
333mod tests {
334    #![allow(clippy::unwrap_used)]
335    use super::*;
336
337    #[test]
338    fn test_password_config_default() {
339        let config = PasswordConfig::default();
340        assert_eq!(config.length, 16);
341        assert!(config.include_uppercase);
342        assert!(config.include_lowercase);
343        assert!(config.include_digits);
344        assert!(config.include_symbols);
345        assert!(config.exclude_ambiguous);
346    }
347
348    #[test]
349    fn test_password_config_new() {
350        let config = PasswordConfig::new();
351        assert_eq!(config.length, 16);
352        assert!(config.include_uppercase);
353        assert!(config.include_lowercase);
354        assert!(config.include_digits);
355        assert!(config.include_symbols);
356        assert!(config.exclude_ambiguous);
357    }
358
359    #[test]
360    fn test_password_config_builder_methods() {
361        let config = PasswordConfig::new()
362            .with_length(24)
363            .with_uppercase(false)
364            .with_lowercase(true)
365            .with_digits(false)
366            .with_symbols(true)
367            .with_exclude_ambiguous(true);
368
369        assert_eq!(config.length, 24);
370        assert!(!config.include_uppercase);
371        assert!(config.include_lowercase);
372        assert!(!config.include_digits);
373        assert!(config.include_symbols);
374        assert!(config.exclude_ambiguous);
375    }
376
377    #[test]
378    fn test_generate_basic_password() {
379        let config = PasswordConfig::default();
380        let password = config.generate().unwrap();
381
382        assert_eq!(password.len(), 16);
383        assert!(!password.is_empty());
384    }
385
386    #[test]
387    fn test_generate_different_lengths() {
388        for length in [4, 8, 12, 16, 32, 64, 128] {
389            let config = PasswordConfig::new().with_length(length);
390            let password = config.generate().unwrap();
391            assert_eq!(password.len(), length);
392        }
393    }
394
395    #[test]
396    fn test_generate_minimum_length() {
397        let config = PasswordConfig::new().with_length(1);
398        let password = config.generate().unwrap();
399        assert_eq!(password.len(), 4);
400    }
401
402    #[test]
403    fn test_generate_maximum_length() {
404        let config = PasswordConfig::new().with_length(128);
405        let password = config.generate().unwrap();
406        assert_eq!(password.len(), 128);
407    }
408
409    #[test]
410    fn test_generate_uppercase_only() {
411        let config = PasswordConfig::new()
412            .with_length(20)
413            .with_uppercase(true)
414            .with_lowercase(false)
415            .with_digits(false)
416            .with_symbols(false);
417
418        let password = config.generate().unwrap();
419        assert_eq!(password.len(), 20);
420        assert!(password.chars().all(|c| c.is_ascii_uppercase()));
421        assert!(password.chars().any(|c| c.is_ascii_alphabetic()));
422    }
423
424    #[test]
425    fn test_generate_lowercase_only() {
426        let config = PasswordConfig::new()
427            .with_length(20)
428            .with_uppercase(false)
429            .with_lowercase(true)
430            .with_digits(false)
431            .with_symbols(false);
432
433        let password = config.generate().unwrap();
434        assert_eq!(password.len(), 20);
435        assert!(password.chars().all(|c| c.is_ascii_lowercase()));
436        assert!(password.chars().any(|c| c.is_ascii_alphabetic()));
437    }
438
439    #[test]
440    fn test_generate_digits_only() {
441        let config = PasswordConfig::new()
442            .with_length(20)
443            .with_uppercase(false)
444            .with_lowercase(false)
445            .with_digits(true)
446            .with_symbols(false);
447
448        let password = config.generate().unwrap();
449        assert_eq!(password.len(), 20);
450        assert!(password.chars().all(|c| c.is_ascii_digit()));
451    }
452
453    #[test]
454    fn test_generate_symbols_only() {
455        let config = PasswordConfig::new()
456            .with_length(20)
457            .with_uppercase(false)
458            .with_lowercase(false)
459            .with_digits(false)
460            .with_symbols(true);
461
462        let password = config.generate().unwrap();
463        assert_eq!(password.len(), 20);
464        assert!(password.chars().all(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
465    }
466
467    #[test]
468    fn test_generate_mixed_character_sets() {
469        let config = PasswordConfig::new()
470            .with_length(100)
471            .with_uppercase(true)
472            .with_lowercase(true)
473            .with_digits(true)
474            .with_symbols(true);
475
476        let password = config.generate().unwrap();
477        assert_eq!(password.len(), 100);
478
479        // With a long password, we should have characters from all sets
480        assert!(password.chars().any(|c| c.is_ascii_uppercase()));
481        assert!(password.chars().any(|c| c.is_ascii_lowercase()));
482        assert!(password.chars().any(|c| c.is_ascii_digit()));
483        assert!(password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
484    }
485
486    #[test]
487    fn test_generate_exclude_ambiguous() {
488        let config = PasswordConfig::new().with_length(100).with_exclude_ambiguous(true);
489
490        let password = config.generate().unwrap();
491        assert_eq!(password.len(), 100);
492
493        // Should not contain ambiguous characters
494        let ambiguous = "0O1lI";
495        assert!(password.chars().all(|c| !ambiguous.contains(c)));
496    }
497
498    #[test]
499    fn test_generate_include_ambiguous() {
500        let config = PasswordConfig::new()
501            .with_length(128) // Large sample to increase chance of ambiguous chars
502            .with_exclude_ambiguous(false);
503
504        let password = config.generate().unwrap();
505        assert_eq!(password.len(), 128);
506
507        // With a large sample and ambiguous characters allowed,
508        // we should likely see at least some ambiguous characters
509        let ambiguous = "0O1lI";
510        let has_ambiguous = password.chars().any(|c| ambiguous.contains(c));
511        // Note: This test might occasionally fail due to randomness, but it's very unlikely
512        assert!(has_ambiguous);
513    }
514
515    #[test]
516    fn test_generate_no_character_sets_enabled() {
517        let config = PasswordConfig::new()
518            .with_length(10)
519            .with_uppercase(false)
520            .with_lowercase(false)
521            .with_digits(false)
522            .with_symbols(false);
523
524        let result = config.generate();
525        assert!(result.is_err());
526        assert!(
527            result
528                .unwrap_err()
529                .to_string()
530                .contains("At least one character set must be enabled")
531        );
532    }
533
534    #[test]
535    fn test_generate_zero_length() {
536        let config = PasswordConfig::new().with_length(0);
537        let password = config.generate().unwrap();
538        assert_eq!(password.len(), 4);
539        assert!(!password.is_empty());
540    }
541
542    #[test]
543    fn test_generate_reproducible_with_different_configs() {
544        // Test that different configs produce different passwords
545        let config1 = PasswordConfig::new().with_length(16).with_symbols(false);
546        let config2 = PasswordConfig::new().with_length(16).with_symbols(true);
547
548        let password1 = config1.generate().unwrap();
549        let password2 = config2.generate().unwrap();
550
551        // While not guaranteed, passwords should very likely be different
552        assert_ne!(password1, password2);
553    }
554
555    #[test]
556    fn test_generate_multiple_passwords_different() {
557        let config = PasswordConfig::new();
558
559        let password1 = config.generate().unwrap();
560        let password2 = config.generate().unwrap();
561        let password3 = config.generate().unwrap();
562
563        // Passwords should be different (very high probability)
564        assert_ne!(password1, password2);
565        assert_ne!(password2, password3);
566        assert_ne!(password1, password3);
567    }
568
569    #[test]
570    fn test_simple_password() {
571        let password = generate_simple_password(12).unwrap();
572        assert_eq!(password.len(), 12);
573        assert!(!password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
574
575        // Should contain letters and digits only
576        assert!(password.chars().all(|c| c.is_ascii_alphanumeric()));
577    }
578
579    #[test]
580    fn test_simple_password_different_lengths() {
581        for length in [4, 8, 16, 32, 64] {
582            let password = generate_simple_password(length).unwrap();
583            assert_eq!(password.len(), length);
584            assert!(!password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
585        }
586    }
587
588    #[test]
589    fn test_simple_password_character_distribution() {
590        // Test with a longer password to check character distribution
591        let password = generate_simple_password(100).unwrap();
592        assert_eq!(password.len(), 100);
593
594        let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase());
595        let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase());
596        let has_digits = password.chars().any(|c| c.is_ascii_digit());
597        let has_symbols = password.chars().any(|c| !c.is_ascii_alphanumeric());
598
599        // Should have letters and digits, but no symbols
600        assert!(has_uppercase || has_lowercase); // Should have some letters
601        assert!(has_digits); // Should have some digits
602        assert!(!has_symbols); // Should not have symbols
603    }
604
605    #[test]
606    fn test_memorable_password_basic() {
607        let password = generate_memorable_password();
608
609        assert!(!password.is_empty());
610        assert!(password.len() >= 8); // Should be reasonably long
611        assert!(password.len() <= 20); // But not too long for memorable
612
613        // Should contain both letters and digits
614        assert!(password.chars().any(|c| c.is_ascii_alphabetic()));
615        assert!(password.chars().any(|c| c.is_ascii_digit()));
616    }
617
618    #[test]
619    fn test_memorable_password_format() {
620        let password = generate_memorable_password();
621
622        // First character should be uppercase (capitalized first syllable)
623        assert!(password.chars().next().unwrap().is_ascii_uppercase());
624
625        // Should contain digits
626        assert!(password.chars().any(|c| c.is_ascii_digit()));
627
628        // Should not contain symbols
629        assert!(password.chars().all(|c| c.is_ascii_alphanumeric()));
630    }
631
632    #[test]
633    fn test_memorable_password_uniqueness() {
634        let password1 = generate_memorable_password();
635        let password2 = generate_memorable_password();
636        let password3 = generate_memorable_password();
637
638        // Should generate different passwords
639        assert_ne!(password1, password2);
640        assert_ne!(password2, password3);
641        assert_ne!(password1, password3);
642    }
643
644    #[test]
645    fn test_memorable_password_syllable_structure() {
646        let password = generate_memorable_password();
647
648        // Remove digits to check syllable structure
649        let letters_only: String = password.chars().filter(char::is_ascii_alphabetic).collect();
650
651        // Should have reasonable length for syllables
652        // Based on the implementation: 2 words × 3-4 syllables × 2 chars per syllable = 12-16 letters
653        assert!(letters_only.len() >= 6);
654        assert!(letters_only.len() <= 20); // Increased upper bound to accommodate actual generation
655
656        // Should be pronounceable (no consecutive consonants that are hard to pronounce)
657        // This is a basic check - real syllable validation would be more complex
658        assert!(!letters_only.is_empty());
659    }
660
661    #[test]
662    fn test_password_consistency_over_multiple_generations() {
663        // Test that the same config produces valid passwords consistently
664        let config = PasswordConfig::new().with_length(20);
665
666        for _ in 0..100 {
667            let password = config.generate().unwrap();
668            assert_eq!(password.len(), 20);
669            assert!(password.is_ascii());
670        }
671    }
672
673    #[test]
674    fn test_extreme_configurations() {
675        // Test edge cases that should still work
676
677        // Only uppercase letters, length 1
678        let config = PasswordConfig::new()
679            .with_length(1)
680            .with_uppercase(true)
681            .with_lowercase(false)
682            .with_digits(false)
683            .with_symbols(false);
684
685        let password = config.generate().unwrap();
686        assert_eq!(password.len(), 4);
687        assert!(password.chars().next().unwrap().is_ascii_uppercase());
688    }
689
690    #[test]
691    fn test_character_set_boundaries() {
692        // Test that all expected characters can appear
693        let config = PasswordConfig::new()
694            .with_length(10000) // Large sample
695            .with_exclude_ambiguous(false);
696
697        let password = config.generate().unwrap();
698
699        // Check that we get characters from expected ranges
700        let has_upper = password.chars().any(|c| c.is_ascii_uppercase());
701        let has_lower = password.chars().any(|c| c.is_ascii_lowercase());
702        let has_digit = password.chars().any(|c| c.is_ascii_digit());
703        let has_symbol = password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c));
704
705        assert!(has_upper);
706        assert!(has_lower);
707        assert!(has_digit);
708        assert!(has_symbol);
709    }
710
711    #[test]
712    fn test_complex_password() {
713        let password = generate_complex_password(16).unwrap();
714        assert_eq!(password.len(), 16);
715
716        // Complex passwords should include ambiguous characters when exclude_ambiguous is false
717        // Test with a larger sample to increase probability
718        let large_password = generate_complex_password(100).unwrap();
719        let ambiguous = "0O1lI";
720        let has_ambiguous = large_password.chars().any(|c| ambiguous.contains(c));
721        assert!(has_ambiguous, "Complex password should include ambiguous characters");
722    }
723
724    #[test]
725    fn test_complex_password_different_lengths() {
726        for length in [4, 8, 16, 32, 64] {
727            let password = generate_complex_password(length).unwrap();
728            assert_eq!(password.len(), length);
729            assert!(!password.is_empty());
730        }
731    }
732
733    #[test]
734    fn test_is_valid_method() {
735        // Test valid configurations
736        let valid_configs = [
737            PasswordConfig::new(),
738            PasswordConfig::new()
739                .with_uppercase(true)
740                .with_lowercase(false)
741                .with_digits(false)
742                .with_symbols(false),
743            PasswordConfig::new()
744                .with_uppercase(false)
745                .with_lowercase(true)
746                .with_digits(false)
747                .with_symbols(false),
748            PasswordConfig::new()
749                .with_uppercase(false)
750                .with_lowercase(false)
751                .with_digits(true)
752                .with_symbols(false),
753            PasswordConfig::new()
754                .with_uppercase(false)
755                .with_lowercase(false)
756                .with_digits(false)
757                .with_symbols(true),
758        ];
759
760        for config in valid_configs {
761            assert!(config.is_valid(), "Configuration should be valid: {config:?}");
762        }
763
764        // Test invalid configuration
765        let invalid_config = PasswordConfig::new()
766            .with_uppercase(false)
767            .with_lowercase(false)
768            .with_digits(false)
769            .with_symbols(false);
770        assert!(
771            !invalid_config.is_valid(),
772            "Configuration with no character sets should be invalid"
773        );
774    }
775
776    #[test]
777    fn test_length_clamping_upper_bound() {
778        let config = PasswordConfig::new().with_length(200); // Above 128
779        let password = config.generate().unwrap();
780        assert_eq!(password.len(), 128); // Should be clamped to 128
781    }
782
783    #[test]
784    fn test_length_clamping_comprehensive() {
785        let test_cases = [
786            (0, 4),      // Below minimum
787            (1, 4),      // Below minimum
788            (3, 4),      // Below minimum
789            (4, 4),      // Minimum
790            (10, 10),    // Normal
791            (64, 64),    // Normal
792            (128, 128),  // Maximum
793            (150, 128),  // Above maximum
794            (1000, 128), // Way above maximum
795        ];
796
797        for (input, expected) in test_cases {
798            let config = PasswordConfig::new().with_length(input);
799            assert_eq!(
800                config.length, expected,
801                "Length {input} should be clamped to {expected}"
802            );
803
804            let password = config.generate().unwrap();
805            assert_eq!(password.len(), expected);
806        }
807    }
808
809    #[test]
810    fn test_builder_method_chaining_comprehensive() {
811        let config = PasswordConfig::new()
812            .with_length(24)
813            .with_uppercase(false)
814            .with_lowercase(true)
815            .with_digits(true)
816            .with_symbols(false)
817            .with_exclude_ambiguous(false);
818
819        let password = config.generate().unwrap();
820        assert_eq!(password.len(), 24);
821
822        // Should only have lowercase letters and digits
823        assert!(password.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
824        assert!(password.chars().any(|c| c.is_ascii_lowercase()));
825        assert!(password.chars().any(|c| c.is_ascii_digit()));
826        assert!(!password.chars().any(|c| c.is_ascii_uppercase()));
827        assert!(!password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
828    }
829
830    #[test]
831    fn test_builder_method_overriding() {
832        let config = PasswordConfig::new()
833            .with_length(10)
834            .with_length(20) // Override previous setting
835            .with_symbols(true)
836            .with_symbols(false); // Override previous setting
837
838        let password = config.generate().unwrap();
839        assert_eq!(password.len(), 20); // Should use the last length setting
840        assert!(!password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c))); // Should not have symbols
841    }
842
843    #[test]
844    fn test_specific_ambiguous_character_exclusion() {
845        let config = PasswordConfig::new()
846            .with_length(200) // Large sample
847            .with_exclude_ambiguous(true);
848
849        let password = config.generate().unwrap();
850
851        // Test specific ambiguous characters are excluded
852        let ambiguous_chars = ['0', 'O', '1', 'l', 'I'];
853        for ch in ambiguous_chars {
854            assert!(
855                !password.contains(ch),
856                "Password should not contain ambiguous character '{ch}'"
857            );
858        }
859    }
860
861    #[test]
862    fn test_specific_ambiguous_character_inclusion() {
863        let config = PasswordConfig::new()
864            .with_length(500) // Very large sample to ensure we get ambiguous chars
865            .with_exclude_ambiguous(false);
866
867        let password = config.generate().unwrap();
868
869        // With a very large sample, we should get at least some ambiguous characters
870        let ambiguous_chars = ['0', 'O', '1', 'l', 'I'];
871        let has_any_ambiguous = ambiguous_chars.iter().any(|&ch| password.contains(ch));
872        assert!(
873            has_any_ambiguous,
874            "Large password should contain at least some ambiguous characters"
875        );
876    }
877
878    #[test]
879    fn test_character_set_exact_contents() {
880        // Test that we get exactly the expected characters for each set
881        let test_cases = [
882            // (config, expected_chars, forbidden_chars)
883            (
884                PasswordConfig::new()
885                    .with_uppercase(true)
886                    .with_lowercase(false)
887                    .with_digits(false)
888                    .with_symbols(false)
889                    .with_exclude_ambiguous(true),
890                "ABCDEFGHJKLMNPQRSTUVWXYZ", // Excludes I, O
891                "IO0123456789!@#$%^&*()_+-=[]{}|;:,.<>?abcdefghijklmnopqrstuvwxyz",
892            ),
893            (
894                PasswordConfig::new()
895                    .with_uppercase(false)
896                    .with_lowercase(true)
897                    .with_digits(false)
898                    .with_symbols(false)
899                    .with_exclude_ambiguous(true),
900                "abcdefghijkmnopqrstuvwxyz", // Excludes l only
901                "lIO0123456789!@#$%^&*()_+-=[]{}|;:,.<>?ABCDEFGHIJKLMNOPQRSTUVWXYZ",
902            ),
903            (
904                PasswordConfig::new()
905                    .with_uppercase(false)
906                    .with_lowercase(false)
907                    .with_digits(true)
908                    .with_symbols(false)
909                    .with_exclude_ambiguous(true),
910                "23456789", // Excludes 0, 1
911                "01lIOi!@#$%^&*()_+-=[]{}|;:,.<>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
912            ),
913            (
914                PasswordConfig::new()
915                    .with_uppercase(false)
916                    .with_lowercase(false)
917                    .with_digits(false)
918                    .with_symbols(true)
919                    .with_exclude_ambiguous(true),
920                "!@#$%^&*+-=?", // Reduced symbol set
921                "()_[]{}|;:,.<>lIO0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
922            ),
923        ];
924
925        for (config, expected_chars, forbidden_chars) in test_cases {
926            let password = config.generate().unwrap();
927
928            // Check that all characters in the password are from the expected set
929            for ch in password.chars() {
930                assert!(
931                    expected_chars.contains(ch),
932                    "Character '{ch}' should be in expected set '{expected_chars}'"
933                );
934                assert!(
935                    !forbidden_chars.contains(ch),
936                    "Character '{ch}' should not be in forbidden set"
937                );
938            }
939        }
940    }
941
942    #[test]
943    fn test_minimum_character_requirements() {
944        // Test that passwords contain at least one character from each enabled set
945        let config = PasswordConfig::new()
946            .with_length(20)
947            .with_uppercase(true)
948            .with_lowercase(true)
949            .with_digits(true)
950            .with_symbols(true);
951
952        // Test multiple generations to ensure consistency
953        for _ in 0..10 {
954            let password = config.generate().unwrap();
955
956            assert!(
957                password.chars().any(|c| c.is_ascii_uppercase()),
958                "Password should contain at least one uppercase letter"
959            );
960            assert!(
961                password.chars().any(|c| c.is_ascii_lowercase()),
962                "Password should contain at least one lowercase letter"
963            );
964            assert!(
965                password.chars().any(|c| c.is_ascii_digit()),
966                "Password should contain at least one digit"
967            );
968            assert!(
969                password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)),
970                "Password should contain at least one symbol"
971            );
972        }
973    }
974
975    #[test]
976    fn test_memorable_password_syllable_count() {
977        let password = generate_memorable_password();
978
979        // Remove digits to get just the syllable part
980        let letters_only: String = password.chars().filter(char::is_ascii_alphabetic).collect();
981
982        // Based on implementation: 2 words × 3-4 syllables × 2 chars per syllable = 12-16 chars
983        assert!(
984            letters_only.len() >= 12,
985            "Should have at least 12 letters from syllables, got {}",
986            letters_only.len()
987        );
988        assert!(
989            letters_only.len() <= 16,
990            "Should have at most 16 letters from syllables, got {}",
991            letters_only.len()
992        );
993    }
994
995    #[test]
996    fn test_memorable_password_digit_count() {
997        let password = generate_memorable_password();
998        let digit_count = password.chars().filter(char::is_ascii_digit).count();
999
1000        // Based on implementation: 1 separator digit + 2 ending digits = 3 total
1001        assert_eq!(digit_count, 3, "Memorable password should have exactly 3 digits");
1002    }
1003
1004    #[test]
1005    fn test_memorable_password_capitalization_pattern() {
1006        for _ in 0..10 {
1007            let password = generate_memorable_password();
1008
1009            // First character should be uppercase
1010            assert!(
1011                password.chars().next().unwrap().is_ascii_uppercase(),
1012                "First character should be uppercase"
1013            );
1014
1015            // Find the first digit (separator)
1016            if let Some(separator_pos) = password.chars().position(|c| c.is_ascii_digit()) {
1017                let chars: Vec<char> = password.chars().collect();
1018
1019                let mut first_syllable_chars = 0;
1020                for (i, &ch) in chars.iter().enumerate() {
1021                    if i >= separator_pos {
1022                        break;
1023                    }
1024                    if ch.is_ascii_alphabetic() {
1025                        first_syllable_chars += 1;
1026                        if first_syllable_chars <= 2 {
1027                            assert!(
1028                                ch.is_ascii_uppercase(),
1029                                "Character '{ch}' at position {i} in first syllable should be uppercase"
1030                            );
1031                        } else {
1032                            assert!(
1033                                ch.is_ascii_lowercase(),
1034                                "Character '{ch}' at position {i} should be lowercase (after first syllable)"
1035                            );
1036                        }
1037                    }
1038                }
1039            }
1040        }
1041    }
1042
1043    #[test]
1044    fn test_memorable_password_structure_consistency() {
1045        // Test the general structure: Word1 + Digit + Word2 + DigitDigit
1046        for _ in 0..20 {
1047            let password = generate_memorable_password();
1048            let chars: Vec<char> = password.chars().collect();
1049
1050            // Should start with uppercase letter
1051            assert!(chars[0].is_ascii_uppercase());
1052
1053            // Should end with two digits
1054            let len = chars.len();
1055            assert!(chars[len - 1].is_ascii_digit(), "Should end with digit");
1056            assert!(chars[len - 2].is_ascii_digit(), "Second to last should be digit");
1057
1058            // Should have exactly 3 digits total
1059            let digit_count = password.chars().filter(char::is_ascii_digit).count();
1060            assert_eq!(digit_count, 3, "Should have exactly 3 digits");
1061        }
1062    }
1063
1064    #[test]
1065    fn test_performance_large_passwords() {
1066        use std::time::Instant;
1067
1068        let start = Instant::now();
1069        let config = PasswordConfig::new().with_length(128);
1070
1071        for _ in 0..100 {
1072            let password = config.generate().unwrap();
1073            assert_eq!(password.len(), 128);
1074        }
1075
1076        let duration = start.elapsed();
1077        assert!(
1078            duration.as_secs() < 5,
1079            "Password generation should complete within 5 seconds"
1080        );
1081    }
1082
1083    #[test]
1084    fn test_character_distribution_balance() {
1085        let config = PasswordConfig::new().with_length(400); // Large sample
1086        let password = config.generate().unwrap();
1087
1088        let uppercase_count = password.chars().filter(char::is_ascii_uppercase).count();
1089        let lowercase_count = password.chars().filter(char::is_ascii_lowercase).count();
1090        let digit_count = password.chars().filter(char::is_ascii_digit).count();
1091        let symbol_count = password
1092            .chars()
1093            .filter(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(*c))
1094            .count();
1095
1096        // With a large sample, each character type should appear reasonably frequently
1097        // Allow for some variance but ensure no type is completely absent or overwhelming
1098        assert!(
1099            uppercase_count >= 20,
1100            "Should have at least 20 uppercase chars, got {uppercase_count}"
1101        );
1102        assert!(
1103            lowercase_count >= 20,
1104            "Should have at least 20 lowercase chars, got {lowercase_count}"
1105        );
1106        assert!(digit_count >= 5, "Should have at least 5 digits, got {digit_count}");
1107        assert!(symbol_count >= 5, "Should have at least 5 symbols, got {symbol_count}");
1108
1109        // No single type should dominate (more than 80% of total)
1110        assert!(uppercase_count < 320, "Uppercase shouldn't dominate");
1111        assert!(lowercase_count < 320, "Lowercase shouldn't dominate");
1112        assert!(digit_count < 320, "Digits shouldn't dominate");
1113        assert!(symbol_count < 320, "Symbols shouldn't dominate");
1114    }
1115
1116    #[test]
1117    fn test_all_builder_methods_together() {
1118        let config = PasswordConfig::new()
1119            .with_length(32)
1120            .with_uppercase(true)
1121            .with_lowercase(true)
1122            .with_digits(true)
1123            .with_symbols(true)
1124            .with_exclude_ambiguous(true);
1125
1126        let password = config.generate().unwrap();
1127        assert_eq!(password.len(), 32);
1128        assert_eq!(config.length, 32);
1129        assert!(config.include_uppercase);
1130        assert!(config.include_lowercase);
1131        assert!(config.include_digits);
1132        assert!(config.include_symbols);
1133        assert!(config.exclude_ambiguous);
1134
1135        // Verify the password follows the configuration
1136        assert!(password.chars().any(|c| c.is_ascii_uppercase()));
1137        assert!(password.chars().any(|c| c.is_ascii_lowercase()));
1138        assert!(password.chars().any(|c| c.is_ascii_digit()));
1139        assert!(password.chars().any(|c| "!@#$%^&*+-=?".contains(c))); // Reduced symbol set
1140
1141        // Should not contain ambiguous characters
1142        let ambiguous = "0O1lI";
1143        assert!(!password.chars().any(|c| ambiguous.contains(c)));
1144    }
1145
1146    #[test]
1147    fn test_password_entropy_distribution() {
1148        // Test that repeated password generation produces varied results
1149        let config = PasswordConfig::new().with_length(16);
1150        let mut passwords = std::collections::HashSet::new();
1151
1152        // Generate many passwords
1153        for _ in 0..100 {
1154            let password = config.generate().unwrap();
1155            passwords.insert(password);
1156        }
1157
1158        // Should have generated mostly unique passwords (very high probability)
1159        assert!(
1160            passwords.len() >= 95,
1161            "Should generate mostly unique passwords, got {} unique out of 100",
1162            passwords.len()
1163        );
1164    }
1165
1166    #[test]
1167    fn test_edge_case_single_character_type_minimum_length() {
1168        let config = PasswordConfig::new()
1169            .with_length(4) // Minimum length
1170            .with_uppercase(true)
1171            .with_lowercase(false)
1172            .with_digits(false)
1173            .with_symbols(false);
1174
1175        let password = config.generate().unwrap();
1176        assert_eq!(password.len(), 4);
1177        assert!(password.chars().all(|c| c.is_ascii_uppercase()));
1178        assert!(password.chars().any(|c| c.is_ascii_uppercase())); // At least one char
1179    }
1180}