swift_mt_message/
sample.rs

1//! Sample generation utilities for SWIFT MT messages and fields
2
3use rand::Rng;
4use std::collections::HashMap;
5
6/// Configuration for generating field samples
7#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
8pub struct FieldConfig {
9    /// Preferred length for generated strings
10    pub length_preference: Option<LengthPreference>,
11    /// Constrain generated values
12    pub value_range: Option<ValueRange>,
13    /// Fixed values to choose from
14    pub fixed_values: Option<Vec<String>>,
15    /// Regex pattern to match (for validation)
16    pub pattern: Option<String>,
17}
18
19/// Configuration for generating message samples
20#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
21pub struct MessageConfig {
22    /// Whether to include optional fields
23    pub include_optional: bool,
24    /// Field-specific configurations
25    pub field_configs: HashMap<String, FieldConfig>,
26    /// Predefined scenario to use
27    pub scenario: Option<MessageScenario>,
28}
29
30/// Predefined message generation scenarios
31#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
32pub enum MessageScenario {
33    /// Basic compliant message
34    Standard,
35    /// Straight Through Processing compliant
36    StpCompliant,
37    /// Cover payment message format
38    CoverPayment,
39    /// Only mandatory fields
40    Minimal,
41    /// All fields populated
42    Full,
43}
44
45/// Length generation preferences
46#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
47pub enum LengthPreference {
48    /// Generate exactly N characters
49    Exact(usize),
50    /// Generate between min and max characters
51    Range(usize, usize),
52    /// Prefer shorter values
53    Short,
54    /// Prefer longer values
55    Long,
56}
57
58/// Value constraints for generation
59#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
60pub enum ValueRange {
61    /// Constrain amount values
62    Amount {
63        min: f64,
64        max: f64,
65        currency: Option<String>,
66    },
67    /// Constrain date values
68    Date { start: String, end: String },
69    /// Constrain integer values
70    Integer { min: i64, max: i64 },
71}
72
73/// Generate random numeric string of specified length
74pub fn generate_numeric(length: usize) -> String {
75    let mut rng = rand::thread_rng();
76    (0..length)
77        .map(|_| rng.gen_range(0..10).to_string())
78        .collect()
79}
80
81/// Generate random alphabetic string of specified length (uppercase)
82pub fn generate_alphabetic(length: usize) -> String {
83    let mut rng = rand::thread_rng();
84    (0..length)
85        .map(|_| {
86            let ch = rng.gen_range(0..26) as u8 + b'A';
87            ch as char
88        })
89        .collect()
90}
91
92/// Generate random alphanumeric string of specified length
93pub fn generate_alphanumeric(length: usize) -> String {
94    let mut rng = rand::thread_rng();
95    let chars: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".chars().collect();
96    (0..length)
97        .map(|_| chars[rng.gen_range(0..chars.len())])
98        .collect()
99}
100
101/// Generate string with any SWIFT-allowed character (reduced special chars for realism)
102pub fn generate_any_character(length: usize) -> String {
103    let mut rng = rand::thread_rng();
104    // Reduce special characters to make output look more realistic
105    let chars: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 /-.,"
106        .chars()
107        .collect();
108    (0..length)
109        .map(|_| chars[rng.gen_range(0..chars.len())])
110        .collect()
111}
112
113/// Generate decimal number with specified total length and decimal places
114pub fn generate_decimal(length: usize, decimals: usize) -> String {
115    generate_decimal_with_range(length, decimals, None, None)
116}
117
118/// Generate decimal number with optional min/max range
119pub fn generate_decimal_with_range(
120    length: usize,
121    decimals: usize,
122    min: Option<f64>,
123    max: Option<f64>,
124) -> String {
125    if decimals >= length {
126        return "0,00".to_string();
127    }
128
129    let mut rng = rand::thread_rng();
130
131    // If min/max are provided, generate within that range
132    if let (Some(min_val), Some(max_val)) = (min, max) {
133        let amount = rng.gen_range(min_val..=max_val);
134        let formatted = format!("{amount:.2}").replace('.', ",");
135        if formatted.len() <= length {
136            return formatted;
137        }
138    }
139
140    // Generate realistic amounts based on typical transaction ranges
141    let realistic_amounts = [
142        "1250,00",
143        "850,50",
144        "2000,75",
145        "500,25",
146        "10000,00",
147        "750,80",
148        "3500,45",
149        "125,60",
150        "25000,00",
151        "1875,90",
152        "650,15",
153        "4200,35",
154        "50000,00",
155        "75000,00",
156        "100000,00",
157        "250000,00",
158        "500000,00",
159        "1000000,00",
160        "2500000,00",
161        "5000000,00",
162    ];
163
164    // For shorter lengths, use predefined realistic amounts
165    if length <= 10 {
166        let amount = realistic_amounts[rng.gen_range(0..realistic_amounts.len())];
167        if amount.len() <= length {
168            return amount.to_string();
169        }
170    }
171
172    // For longer amounts, generate realistic business transaction amounts
173    // Typical ranges: small (100-9,999), medium (10,000-999,999), large (1M-100M)
174    let amount_ranges = [
175        (100.0, 9999.0),        // Small transactions
176        (10000.0, 99999.0),     // Medium transactions
177        (100000.0, 999999.0),   // Large transactions
178        (1000000.0, 9999999.0), // Very large transactions
179    ];
180
181    let (min_amt, max_amt) = amount_ranges[rng.gen_range(0..amount_ranges.len())];
182    let amount = rng.gen_range(min_amt..=max_amt);
183    let formatted = format!("{amount:.2}").replace('.', ",");
184
185    if formatted.len() <= length {
186        return formatted;
187    }
188
189    // Fallback: generate basic structure if formatted amount is too long
190    let integer_part_len = if length > decimals + 1 {
191        length - decimals - 1 // -1 for comma
192    } else {
193        1 // Ensure at least 1 digit for integer part
194    };
195
196    let mut integer_part = String::new();
197    if integer_part_len > 0 {
198        // Generate smaller realistic amounts that fit the length constraint
199        let max_val = 10_u64.pow(integer_part_len as u32) - 1;
200        let amount = rng.gen_range(100..=max_val.min(999999)); // Cap at reasonable amount
201        integer_part = amount.to_string();
202
203        // Pad if necessary
204        while integer_part.len() < integer_part_len {
205            integer_part = format!("0{integer_part}");
206        }
207    } else {
208        // Fallback: ensure at least one digit
209        integer_part.push_str(&rng.gen_range(1..10).to_string());
210    }
211
212    let decimal_part = generate_numeric(decimals);
213    format!("{integer_part},{decimal_part}")
214}
215
216/// Generate a valid BIC code
217pub fn generate_valid_bic() -> String {
218    let mut rng = rand::thread_rng();
219    let bics = [
220        // Major US banks (all 8 chars)
221        "CHASUS33", "BOFAUS3N", "CITIUS33", "WFBIUS6W", "USBKUS44", "PNCCUS33",
222        // Major European banks (all 8 chars)
223        "DEUTDEFF", "HSBCGB2L", "BNPAFRPP", "UBSWCHZH", "ABNANL2A", "INGBNL2A", "CRESCHZZ",
224        "BARCGB22", "LOYDGB2L", "NWBKGB2L", "RBOSGB2L",
225        // Major Asian banks (all 8 chars)
226        "SCBLSGSG", "DBSSSGSG", "OCBCSGSG", "HSBCHKHH", "CITIHKAX", "BOTKJPJT", "SMFGJPJT",
227        "MHCBJPJT", // Major Canadian/Australian banks (all 8 chars)
228        "ROYCCAT2", "BOFACATT", "ANZBAU3M", "CTBAAU2S",
229        // Major international banks (all 8 chars)
230        "ICICINBB", "HDFCINBB", "SBININBB", "BBVASPBX",
231    ];
232    bics[rng.gen_range(0..bics.len())].to_string()
233}
234
235/// Generate a valid currency code with realistic distribution
236pub fn generate_valid_currency() -> String {
237    let mut rng = rand::thread_rng();
238
239    // Weight currencies by real-world usage in international payments
240    let weighted_selection = rng.gen_range(1..=100);
241
242    match weighted_selection {
243        1..=30 => "USD".to_string(),  // 30% - Most common
244        31..=45 => "EUR".to_string(), // 15% - Second most common
245        46..=55 => "GBP".to_string(), // 10% - Third most common
246        56..=60 => "JPY".to_string(), // 5%
247        61..=64 => "CHF".to_string(), // 4%
248        65..=67 => "CAD".to_string(), // 3%
249        68..=70 => "AUD".to_string(), // 3%
250        71..=73 => "SGD".to_string(), // 3%
251        74..=76 => "HKD".to_string(), // 3%
252        77..=79 => "CNY".to_string(), // 3%
253        80..=82 => "SEK".to_string(), // 3%
254        83..=85 => "NOK".to_string(), // 3%
255        86..=87 => "DKK".to_string(), // 2%
256        88..=89 => "NZD".to_string(), // 2%
257        90..=91 => "INR".to_string(), // 2%
258        92..=93 => "KRW".to_string(), // 2%
259        94..=95 => "BRL".to_string(), // 2%
260        96..=97 => "ZAR".to_string(), // 2%
261        98..=99 => "AED".to_string(), // 2%
262        _ => "MXN".to_string(),       // 1%
263    }
264}
265
266/// Generate a valid country code
267pub fn generate_valid_country_code() -> String {
268    let mut rng = rand::thread_rng();
269    let countries = vec![
270        "US", "GB", "DE", "FR", "IT", "ES", "NL", "BE", "CH", "AT", "JP", "CN", "IN", "AU", "CA",
271        "BR", "MX", "SG", "HK", "KR",
272    ];
273    countries[rng.gen_range(0..countries.len())].to_string()
274}
275
276/// Generate a valid date in YYMMDD format
277pub fn generate_date_yymmdd() -> String {
278    let mut rng = rand::thread_rng();
279    let year = rng.gen_range(20..30);
280    let month = rng.gen_range(1..=12);
281    let day = match month {
282        2 => rng.gen_range(1..=28),
283        4 | 6 | 9 | 11 => rng.gen_range(1..=30),
284        _ => rng.gen_range(1..=31),
285    };
286    format!("{year:02}{month:02}{day:02}")
287}
288
289/// Generate a valid date in YYYYMMDD format
290pub fn generate_date_yyyymmdd() -> String {
291    let mut rng = rand::thread_rng();
292    let year = rng.gen_range(2020..2030);
293    let month = rng.gen_range(1..=12);
294    let day = match month {
295        2 => rng.gen_range(1..=28),
296        4 | 6 | 9 | 11 => rng.gen_range(1..=30),
297        _ => rng.gen_range(1..=31),
298    };
299    format!("{year:04}{month:02}{day:02}")
300}
301
302/// Generate a valid time in HHMM format
303pub fn generate_time_hhmm() -> String {
304    let mut rng = rand::thread_rng();
305    let hour = rng.gen_range(0..24);
306    let minute = rng.gen_range(0..60);
307    format!("{hour:02}{minute:02}")
308}
309
310/// Generate a value based on SWIFT format specification
311pub fn generate_by_format_spec(format: &str) -> String {
312    generate_by_format_spec_with_config(format, &FieldConfig::default())
313}
314
315/// Generate a value based on SWIFT format specification with configuration
316pub fn generate_by_format_spec_with_config(format: &str, config: &FieldConfig) -> String {
317    // Check if fixed values are provided
318    if let Some(fixed_values) = &config.fixed_values {
319        if !fixed_values.is_empty() {
320            let mut rng = rand::thread_rng();
321            return fixed_values[rng.gen_range(0..fixed_values.len())].clone();
322        }
323    }
324
325    // Parse format like "3!a", "6!n", "16x", "15d"
326    let mut chars = format.chars().peekable();
327    let mut length_str = String::new();
328    let mut is_exact = false;
329    let mut char_type = 'x';
330
331    // Parse length
332    while let Some(&ch) = chars.peek() {
333        if ch.is_ascii_digit() {
334            length_str.push(ch);
335            chars.next();
336        } else {
337            break;
338        }
339    }
340
341    // Parse exact indicator
342    if chars.peek() == Some(&'!') {
343        is_exact = true;
344        chars.next();
345    }
346
347    // Parse character type
348    if let Some(ch) = chars.next() {
349        char_type = ch;
350    }
351
352    let max_length: usize = length_str.parse().unwrap_or(1);
353
354    // Apply length preference from config
355    let length = match &config.length_preference {
356        Some(LengthPreference::Exact(len)) => *len.min(&max_length),
357        Some(LengthPreference::Range(min, max)) => {
358            let mut rng = rand::thread_rng();
359            let actual_min = *min.min(&max_length);
360            let actual_max = (*max).min(max_length);
361            if actual_min <= actual_max {
362                rng.gen_range(actual_min..=actual_max)
363            } else {
364                max_length
365            }
366        }
367        Some(LengthPreference::Short) => {
368            let mut rng = rand::thread_rng();
369            rng.gen_range(1..=(max_length / 2).max(1))
370        }
371        Some(LengthPreference::Long) => {
372            let mut rng = rand::thread_rng();
373            rng.gen_range((max_length / 2).max(1)..=max_length)
374        }
375        None => {
376            if is_exact {
377                max_length
378            } else {
379                // For decimal formats, use full length to ensure reasonable amounts
380                if char_type == 'd' {
381                    max_length
382                } else {
383                    let mut rng = rand::thread_rng();
384                    rng.gen_range(1..=max_length)
385                }
386            }
387        }
388    };
389
390    match char_type {
391        'n' => generate_numeric(length),
392        'a' => generate_alphabetic(length),
393        'c' => generate_alphanumeric(length),
394        'd' => {
395            // For decimal format, assume 2 decimal places if not specified
396            let decimals = 2;
397            // Check for amount range configuration
398            if let Some(ValueRange::Amount { min, max, .. }) = &config.value_range {
399                generate_decimal_with_range(length, decimals, Some(*min), Some(*max))
400            } else {
401                generate_decimal(length, decimals)
402            }
403        }
404        _ => generate_any_character(length),
405    }
406}
407
408/// Generate an account number (max 34 characters)
409pub fn generate_account_number() -> String {
410    let mut rng = rand::thread_rng();
411    let length = rng.gen_range(10..=34);
412    generate_alphanumeric(length)
413}
414
415/// Generate a reference number (16 characters)
416pub fn generate_reference() -> String {
417    generate_alphanumeric(16)
418}
419
420/// Generate a transaction code
421pub fn generate_transaction_code() -> String {
422    let mut rng = rand::thread_rng();
423    let codes = ["NTRF", "CHQB", "PMNT", "MCOP", "DMCT"];
424    codes[rng.gen_range(0..codes.len())].to_string()
425}
426
427/// Generate a bank operation code
428pub fn generate_bank_operation_code() -> String {
429    let mut rng = rand::thread_rng();
430    let codes = ["CRED", "CRTS", "SPAY", "SSTD"];
431    codes[rng.gen_range(0..codes.len())].to_string()
432}
433
434/// Generate a details of charges code
435pub fn generate_details_of_charges() -> String {
436    let mut rng = rand::thread_rng();
437    let codes = ["BEN", "OUR", "SHA"];
438    codes[rng.gen_range(0..codes.len())].to_string()
439}
440
441/// Generate an instruction code
442pub fn generate_instruction_code() -> String {
443    let mut rng = rand::thread_rng();
444    let codes = ["REPA", "URGP", "CORT", "INTC", "PHON"];
445    codes[rng.gen_range(0..codes.len())].to_string()
446}
447
448/// Generate name and address lines
449pub fn generate_name_and_address(lines: usize) -> Vec<String> {
450    let mut rng = rand::thread_rng();
451    let names = [
452        "GLOBAL TRADE SOLUTIONS LTD",
453        "INTERNATIONAL EXPORT CORP",
454        "PRIME FINANCIAL SERVICES",
455        "METROPOLITAN TRADING CO",
456        "CONSOLIDATED INDUSTRIES INC",
457        "PACIFIC RIM ENTERPRISES",
458        "EUROPEAN COMMERCE GROUP",
459        "ATLANTIC BUSINESS PARTNERS",
460        "CONTINENTAL HOLDINGS LLC",
461        "WORLDWIDE LOGISTICS CORP",
462        "STERLING INVESTMENT GROUP",
463        "MERIDIAN COMMERCIAL LTD",
464        "APEX TRADING COMPANY",
465        "NEXUS FINANCIAL CORP",
466        "HORIZON BUSINESS SOLUTIONS",
467    ];
468
469    let streets = [
470        "125 CORPORATE PLAZA",
471        "450 BUSINESS PARK DRIVE",
472        "789 FINANCIAL DISTRICT",
473        "1200 COMMERCE STREET",
474        "650 EXECUTIVE BOULEVARD",
475        "300 TRADE CENTER WAY",
476        "850 INTERNATIONAL AVENUE",
477        "1500 ENTERPRISE PARKWAY",
478        "275 INVESTMENT PLAZA",
479        "920 BANKING SQUARE",
480        "1750 CORPORATE CENTER",
481        "425 PROFESSIONAL DRIVE",
482        "680 MARKET STREET",
483        "1100 INDUSTRIAL WAY",
484        "550 COMMERCIAL BOULEVARD",
485    ];
486
487    let cities = [
488        "NEW YORK NY 10005",
489        "LONDON EC2V 8RF",
490        "ZURICH 8001",
491        "SINGAPORE 048624",
492        "TOKYO 100-6590",
493        "FRANKFURT AM MAIN 60311",
494        "PARIS 75001",
495        "MILAN 20121",
496        "GENEVA 1204",
497        "DUBLIN 2",
498        "AMSTERDAM 1017 XX",
499        "BRUSSELS 1000",
500        "MADRID 28001",
501        "BARCELONA 08002",
502        "VIENNA 1010",
503    ];
504
505    let mut result = vec![];
506
507    if lines > 0 {
508        result.push(names[rng.gen_range(0..names.len())].to_string());
509    }
510    if lines > 1 {
511        result.push(streets[rng.gen_range(0..streets.len())].to_string());
512    }
513    if lines > 2 {
514        result.push(cities[rng.gen_range(0..cities.len())].to_string());
515    }
516    if lines > 3 {
517        result.push(generate_valid_country_code());
518    }
519
520    // Fill remaining lines with additional address details if needed
521    while result.len() < lines {
522        let additional_info = [
523            "CORPORATE HEADQUARTERS",
524            "MAIN OFFICE",
525            "TREASURY DEPARTMENT",
526            "INTERNATIONAL DIVISION",
527            "FINANCIAL SERVICES",
528        ];
529        result.push(additional_info[rng.gen_range(0..additional_info.len())].to_string());
530    }
531
532    result
533}
534
535/// Generate a UETR (Unique End-to-End Transaction Reference) in UUID format
536/// Used for CBPR+ compliance in Tag 121 of User Header
537pub fn generate_uetr() -> String {
538    uuid::Uuid::new_v4().to_string()
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn test_generate_numeric() {
547        let result = generate_numeric(6);
548        assert_eq!(result.len(), 6);
549        assert!(result.chars().all(|c| c.is_ascii_digit()));
550
551        // Test multiple generations for consistency
552        for _ in 0..10 {
553            let result = generate_numeric(8);
554            assert_eq!(result.len(), 8);
555            assert!(result.chars().all(|c| c.is_ascii_digit()));
556        }
557    }
558
559    #[test]
560    fn test_generate_alphabetic() {
561        let result = generate_alphabetic(4);
562        assert_eq!(result.len(), 4);
563        assert!(result.chars().all(|c| c.is_ascii_uppercase()));
564
565        // Test edge cases
566        let empty = generate_alphabetic(0);
567        assert_eq!(empty.len(), 0);
568
569        let single = generate_alphabetic(1);
570        assert_eq!(single.len(), 1);
571        assert!(single.chars().all(|c| c.is_ascii_uppercase()));
572    }
573
574    #[test]
575    fn test_generate_alphanumeric() {
576        let result = generate_alphanumeric(10);
577        assert_eq!(result.len(), 10);
578        assert!(
579            result.chars().all(
580                |c| c.is_ascii_alphanumeric() && (c.is_ascii_uppercase() || c.is_ascii_digit())
581            )
582        );
583    }
584
585    #[test]
586    fn test_generate_any_character() {
587        let result = generate_any_character(20);
588        assert_eq!(result.len(), 20);
589
590        // All characters should be SWIFT-allowed (reduced set for realism)
591        let allowed_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 /-.,";
592        assert!(result.chars().all(|c| allowed_chars.contains(c)));
593    }
594
595    #[test]
596    fn test_generate_decimal() {
597        let result = generate_decimal(10, 2);
598        assert!(result.contains(','));
599        let parts: Vec<&str> = result.split(',').collect();
600        assert_eq!(parts.len(), 2);
601        assert_eq!(parts[1].len(), 2);
602        assert!(parts[0].chars().all(|c| c.is_ascii_digit()));
603        assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
604
605        // Test edge case - decimals >= length
606        let edge_case = generate_decimal(3, 3);
607        assert_eq!(edge_case, "0,00");
608    }
609
610    #[test]
611    fn test_generate_valid_bic() {
612        for _ in 0..20 {
613            let bic = generate_valid_bic();
614            assert!(bic.len() == 8 || bic.len() == 11);
615            assert!(bic.chars().all(|c| c.is_ascii_alphanumeric()));
616        }
617    }
618
619    #[test]
620    fn test_generate_valid_currency() {
621        for _ in 0..20 {
622            let currency = generate_valid_currency();
623            assert_eq!(currency.len(), 3);
624            assert!(currency.chars().all(|c| c.is_ascii_uppercase()));
625        }
626
627        // Check that it's from the expected list
628        let currencies = vec![
629            "USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD", "SEK", "NOK", "DKK", "SGD",
630            "HKD", "CNY", "INR", "KRW", "MXN", "BRL", "ZAR", "AED",
631        ];
632        let generated = generate_valid_currency();
633        assert!(currencies.contains(&generated.as_str()));
634    }
635
636    #[test]
637    fn test_generate_valid_country_code() {
638        let country = generate_valid_country_code();
639        assert_eq!(country.len(), 2);
640        assert!(country.chars().all(|c| c.is_ascii_uppercase()));
641    }
642
643    #[test]
644    fn test_generate_date_yymmdd() {
645        let date = generate_date_yymmdd();
646        assert_eq!(date.len(), 6);
647        assert!(date.chars().all(|c| c.is_ascii_digit()));
648
649        // Validate format - YYMMDD
650        let year: u32 = date[0..2].parse().unwrap();
651        let month: u32 = date[2..4].parse().unwrap();
652        let day: u32 = date[4..6].parse().unwrap();
653
654        assert!((20..=29).contains(&year));
655        assert!((1..=12).contains(&month));
656        assert!((1..=31).contains(&day));
657    }
658
659    #[test]
660    fn test_generate_date_yyyymmdd() {
661        let date = generate_date_yyyymmdd();
662        assert_eq!(date.len(), 8);
663        assert!(date.chars().all(|c| c.is_ascii_digit()));
664
665        // Validate format - YYYYMMDD
666        let year: u32 = date[0..4].parse().unwrap();
667        let month: u32 = date[4..6].parse().unwrap();
668        let day: u32 = date[6..8].parse().unwrap();
669
670        assert!((2020..=2029).contains(&year));
671        assert!((1..=12).contains(&month));
672        assert!((1..=31).contains(&day));
673    }
674
675    #[test]
676    fn test_generate_time_hhmm() {
677        let time = generate_time_hhmm();
678        assert_eq!(time.len(), 4);
679        assert!(time.chars().all(|c| c.is_ascii_digit()));
680
681        // Validate format - HHMM
682        let hour: u32 = time[0..2].parse().unwrap();
683        let minute: u32 = time[2..4].parse().unwrap();
684
685        assert!(hour <= 23);
686        assert!(minute <= 59);
687    }
688
689    #[test]
690    fn test_generate_by_format_spec() {
691        // Test exact length formats
692        let result1 = generate_by_format_spec("3!a");
693        assert_eq!(result1.len(), 3);
694        assert!(result1.chars().all(|c| c.is_ascii_uppercase()));
695
696        let result2 = generate_by_format_spec("6!n");
697        assert_eq!(result2.len(), 6);
698        assert!(result2.chars().all(|c| c.is_ascii_digit()));
699
700        let result3 = generate_by_format_spec("4!c");
701        assert_eq!(result3.len(), 4);
702        assert!(result3.chars().all(|c| c.is_ascii_alphanumeric()));
703
704        // Test variable length formats
705        let result4 = generate_by_format_spec("16x");
706        assert!(!result4.is_empty() && result4.len() <= 16);
707
708        let result5 = generate_by_format_spec("35a");
709        assert!(!result5.is_empty() && result5.len() <= 35);
710        assert!(result5.chars().all(|c| c.is_ascii_uppercase()));
711
712        // Test decimal format
713        let result6 = generate_by_format_spec("15d");
714        assert!(result6.contains(','));
715    }
716
717    #[test]
718    fn test_generate_account_number() {
719        let account = generate_account_number();
720        assert!(account.len() >= 10 && account.len() <= 34);
721        assert!(account.chars().all(|c| c.is_ascii_alphanumeric()));
722    }
723
724    #[test]
725    fn test_generate_reference() {
726        let reference = generate_reference();
727        assert_eq!(reference.len(), 16);
728        assert!(reference.chars().all(|c| c.is_ascii_alphanumeric()));
729    }
730
731    #[test]
732    fn test_generate_transaction_code() {
733        let codes = ["NTRF", "CHQB", "PMNT", "MCOP", "DMCT"];
734        let code = generate_transaction_code();
735        assert!(codes.contains(&code.as_str()));
736    }
737
738    #[test]
739    fn test_generate_bank_operation_code() {
740        let codes = ["CRED", "CRTS", "SPAY", "SSTD"];
741        let code = generate_bank_operation_code();
742        assert!(codes.contains(&code.as_str()));
743    }
744
745    #[test]
746    fn test_generate_details_of_charges() {
747        let codes = ["BEN", "OUR", "SHA"];
748        let code = generate_details_of_charges();
749        assert!(codes.contains(&code.as_str()));
750    }
751
752    #[test]
753    fn test_generate_instruction_code() {
754        let codes = ["REPA", "URGP", "CORT", "INTC", "PHON"];
755        let code = generate_instruction_code();
756        assert!(codes.contains(&code.as_str()));
757    }
758
759    #[test]
760    fn test_generate_name_and_address() {
761        // Test different line counts
762        for line_count in 1..=5 {
763            let lines = generate_name_and_address(line_count);
764            assert_eq!(lines.len(), line_count);
765            assert!(lines.iter().all(|line| !line.is_empty()));
766            assert!(lines.iter().all(|line| line.len() <= 35)); // SWIFT line length limit
767        }
768
769        // Test edge case - zero lines
770        let empty_lines = generate_name_and_address(0);
771        assert_eq!(empty_lines.len(), 0);
772    }
773
774    #[test]
775    fn test_configuration_types() {
776        // Test FieldConfig creation
777        let field_config = FieldConfig {
778            length_preference: Some(LengthPreference::Exact(10)),
779            value_range: Some(ValueRange::Amount {
780                min: 100.0,
781                max: 1000.0,
782                currency: Some("USD".to_string()),
783            }),
784            fixed_values: Some(vec!["TEST1".to_string(), "TEST2".to_string()]),
785            pattern: Some(r"^[A-Z]{3}\d{7}$".to_string()),
786        };
787
788        assert!(field_config.length_preference.is_some());
789        assert!(field_config.value_range.is_some());
790        assert!(field_config.fixed_values.is_some());
791        assert!(field_config.pattern.is_some());
792
793        // Test MessageConfig creation
794        let mut field_configs = std::collections::HashMap::new();
795        field_configs.insert("32A".to_string(), field_config);
796
797        let message_config = MessageConfig {
798            include_optional: true,
799            field_configs,
800            scenario: Some(MessageScenario::StpCompliant),
801        };
802
803        assert!(message_config.include_optional);
804        assert!(message_config.field_configs.contains_key("32A"));
805        assert_eq!(message_config.scenario, Some(MessageScenario::StpCompliant));
806    }
807
808    #[test]
809    fn test_message_scenarios() {
810        // Test all scenario variants
811        let scenarios = vec![
812            MessageScenario::Standard,
813            MessageScenario::StpCompliant,
814            MessageScenario::CoverPayment,
815            MessageScenario::Minimal,
816            MessageScenario::Full,
817        ];
818
819        for scenario in scenarios {
820            // Just test that they can be created and compared
821            assert_eq!(scenario, scenario);
822        }
823    }
824
825    #[test]
826    fn test_value_range_variants() {
827        // Test Amount range
828        let amount_range = ValueRange::Amount {
829            min: 100.0,
830            max: 1000.0,
831            currency: Some("EUR".to_string()),
832        };
833
834        match amount_range {
835            ValueRange::Amount { min, max, currency } => {
836                assert_eq!(min, 100.0);
837                assert_eq!(max, 1000.0);
838                assert_eq!(currency, Some("EUR".to_string()));
839            }
840            _ => panic!("Expected Amount variant"),
841        }
842
843        // Test Date range
844        let date_range = ValueRange::Date {
845            start: "20230101".to_string(),
846            end: "20231231".to_string(),
847        };
848
849        match date_range {
850            ValueRange::Date { start, end } => {
851                assert_eq!(start, "20230101");
852                assert_eq!(end, "20231231");
853            }
854            _ => panic!("Expected Date variant"),
855        }
856
857        // Test Integer range
858        let integer_range = ValueRange::Integer { min: 1, max: 100 };
859
860        match integer_range {
861            ValueRange::Integer { min, max } => {
862                assert_eq!(min, 1);
863                assert_eq!(max, 100);
864            }
865            _ => panic!("Expected Integer variant"),
866        }
867    }
868
869    #[test]
870    fn test_length_preference_variants() {
871        // Test all LengthPreference variants
872        let exact = LengthPreference::Exact(10);
873        assert_eq!(exact, LengthPreference::Exact(10));
874
875        let range = LengthPreference::Range(5, 15);
876        assert_eq!(range, LengthPreference::Range(5, 15));
877
878        let short = LengthPreference::Short;
879        assert_eq!(short, LengthPreference::Short);
880
881        let long = LengthPreference::Long;
882        assert_eq!(long, LengthPreference::Long);
883    }
884
885    #[test]
886    fn test_randomness_distribution() {
887        // Test that the generators produce different values over multiple runs
888        let mut bics = std::collections::HashSet::new();
889        let mut currencies = std::collections::HashSet::new();
890        let mut references = std::collections::HashSet::new();
891
892        for _ in 0..50 {
893            bics.insert(generate_valid_bic());
894            currencies.insert(generate_valid_currency());
895            references.insert(generate_reference());
896        }
897
898        // We should have some variety (not all the same)
899        assert!(bics.len() > 1, "BIC generation should have variety");
900        assert!(
901            currencies.len() > 1,
902            "Currency generation should have variety"
903        );
904        assert!(
905            references.len() > 1,
906            "Reference generation should have variety"
907        );
908    }
909}