ass_core/utils/
benchmark_generators.rs

1//! Benchmark utilities for generating synthetic ASS scripts
2//!
3//! This module provides generators for creating test ASS scripts with varying
4//! complexity levels, used primarily for benchmarking parser performance.
5//! All generators produce valid ASS format strings that can be parsed by
6//! the core parser.
7
8#[cfg(not(feature = "std"))]
9extern crate alloc;
10use crate::parser::{
11    ast::{EventType, Span},
12    Event,
13};
14#[cfg(not(feature = "std"))]
15use alloc::{
16    fmt::Write,
17    format,
18    string::{String, ToString},
19    vec::Vec,
20};
21#[cfg(feature = "std")]
22use std::fmt::Write;
23
24/// Synthetic ASS script generator for benchmarking
25pub struct ScriptGenerator {
26    /// Script title for metadata
27    pub title: String,
28    /// Number of styles to generate
29    pub styles_count: usize,
30    /// Number of events to generate
31    pub events_count: usize,
32    /// Complexity level for generated content
33    pub complexity_level: ComplexityLevel,
34}
35
36/// Script complexity levels for testing
37#[derive(Debug, Clone, Copy)]
38pub enum ComplexityLevel {
39    /// Simple text with minimal formatting
40    Simple,
41    /// Moderate formatting and some animations
42    Moderate,
43    /// Heavy animations, complex styling, karaoke
44    Complex,
45    /// Extreme complexity to stress-test parser
46    Extreme,
47    /// Anime subtitles - heavy styling and effects
48    AnimeRealistic,
49    /// Movie subtitles - simple, timing-focused
50    MovieRealistic,
51    /// Karaoke files - complex animations and timing
52    KaraokeRealistic,
53    /// Sign translations - positioning and styling
54    SignRealistic,
55    /// Educational content - long dialogues and formatting
56    EducationalRealistic,
57}
58
59impl ScriptGenerator {
60    /// Create generator for simple scripts
61    #[must_use]
62    pub fn simple(events_count: usize) -> Self {
63        Self {
64            title: "Simple Benchmark Script".to_string(),
65            styles_count: 1,
66            events_count,
67            complexity_level: ComplexityLevel::Simple,
68        }
69    }
70
71    /// Create generator for moderate complexity scripts
72    #[must_use]
73    pub fn moderate(events_count: usize) -> Self {
74        Self {
75            title: "Moderate Benchmark Script".to_string(),
76            styles_count: 5,
77            events_count,
78            complexity_level: ComplexityLevel::Moderate,
79        }
80    }
81
82    /// Create generator for complex scripts
83    #[must_use]
84    pub fn complex(events_count: usize) -> Self {
85        Self {
86            title: "Complex Benchmark Script".to_string(),
87            styles_count: 10,
88            events_count,
89            complexity_level: ComplexityLevel::Complex,
90        }
91    }
92
93    /// Create generator for extreme complexity scripts
94    #[must_use]
95    pub fn extreme(events_count: usize) -> Self {
96        Self {
97            title: "Extreme Benchmark Script".to_string(),
98            styles_count: 20,
99            events_count,
100            complexity_level: ComplexityLevel::Extreme,
101        }
102    }
103
104    /// Create generator for anime-style subtitles
105    #[must_use]
106    pub fn anime_realistic(events_count: usize) -> Self {
107        Self {
108            title: "Anime Subtitles".to_string(),
109            styles_count: 15,
110            events_count,
111            complexity_level: ComplexityLevel::AnimeRealistic,
112        }
113    }
114
115    /// Create generator for movie subtitles
116    #[must_use]
117    pub fn movie_realistic(events_count: usize) -> Self {
118        Self {
119            title: "Movie Subtitles".to_string(),
120            styles_count: 3,
121            events_count,
122            complexity_level: ComplexityLevel::MovieRealistic,
123        }
124    }
125
126    /// Create generator for karaoke files
127    #[must_use]
128    pub fn karaoke_realistic(events_count: usize) -> Self {
129        Self {
130            title: "Karaoke Script".to_string(),
131            styles_count: 8,
132            events_count,
133            complexity_level: ComplexityLevel::KaraokeRealistic,
134        }
135    }
136
137    /// Create generator for sign translation subtitles
138    #[must_use]
139    pub fn sign_realistic(events_count: usize) -> Self {
140        Self {
141            title: "Sign Translation".to_string(),
142            styles_count: 12,
143            events_count,
144            complexity_level: ComplexityLevel::SignRealistic,
145        }
146    }
147
148    /// Create generator for educational content
149    #[must_use]
150    pub fn educational_realistic(events_count: usize) -> Self {
151        Self {
152            title: "Educational Content".to_string(),
153            styles_count: 6,
154            events_count,
155            complexity_level: ComplexityLevel::EducationalRealistic,
156        }
157    }
158
159    /// Generate complete ASS script as string
160    #[must_use]
161    pub fn generate(&self) -> String {
162        let mut script =
163            String::with_capacity(1000 + (self.styles_count * 200) + (self.events_count * 150));
164
165        // Script Info section
166        script.push_str(&self.generate_script_info());
167        script.push('\n');
168
169        // V4+ Styles section
170        script.push_str(&self.generate_styles());
171        script.push('\n');
172
173        // Events section
174        script.push_str(&self.generate_events());
175
176        script
177    }
178
179    /// Generate Script Info section
180    fn generate_script_info(&self) -> String {
181        format!(
182            r"[Script Info]
183Title: {}
184ScriptType: v4.00+
185WrapStyle: 0
186ScaledBorderAndShadow: yes
187PlayResX: 1920
188PlayResY: 1080",
189            self.title
190        )
191    }
192
193    /// Generate V4+ Styles section
194    fn generate_styles(&self) -> String {
195        let mut styles = String::from(
196            "[V4+ Styles]\n\
197            Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
198        );
199
200        for i in 0..self.styles_count {
201            let style_name_string;
202            let style_name = if i == 0 {
203                "Default"
204            } else {
205                style_name_string = format!("Style{i}");
206                &style_name_string
207            };
208            let fontsize = 20 + (i * 2);
209            let color = format!("&H00{:06X}&", i * 0x0011_1111);
210
211            writeln!(
212                styles,
213                "Style: {style_name},Arial,{fontsize},{color},{color},{color},&H00000000&,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1"
214            ).unwrap();
215        }
216
217        styles
218    }
219
220    /// Generate Events section
221    fn generate_events(&self) -> String {
222        let mut events = String::from(
223            "[Events]\n\
224            Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n",
225        );
226
227        for i in 0..self.events_count {
228            let start_cs = u32::try_from(i * 3000).unwrap_or(u32::MAX);
229            let end_cs = u32::try_from(i * 3000 + 2500).unwrap_or(u32::MAX);
230            let start_time = Self::format_time(start_cs); // 3 seconds apart
231            let end_time = Self::format_time(end_cs); // 2.5 second duration
232            let style = if self.styles_count > 1 {
233                format!("Style{}", i % self.styles_count)
234            } else {
235                "Default".to_string()
236            };
237            let text = self.generate_dialogue_text(i);
238
239            writeln!(
240                events,
241                "Dialogue: 0,{start_time},{end_time},{style},Speaker,0,0,0,,{text}"
242            )
243            .unwrap();
244        }
245
246        events
247    }
248
249    /// Format time in ASS format (H:MM:SS.cc)
250    fn format_time(centiseconds: u32) -> String {
251        let hours = centiseconds / 360_000;
252        let minutes = (centiseconds % 360_000) / 6_000;
253        let seconds = (centiseconds % 6000) / 100;
254        let cs = centiseconds % 100;
255        format!("{hours}:{minutes:02}:{seconds:02}.{cs:02}")
256    }
257
258    /// Generate dialogue text based on complexity level
259    fn generate_dialogue_text(&self, event_index: usize) -> String {
260        let base_text = format!("This is dialogue line number {}", event_index + 1);
261
262        match self.complexity_level {
263            ComplexityLevel::Simple => base_text,
264            ComplexityLevel::Moderate => {
265                format!(r"{{\b1}}{base_text}{{\b0}} with {{\i1}}some{{\i0}} formatting")
266            }
267            ComplexityLevel::Complex => {
268                format!(
269                    r"{{\pos(100,200)\fad(500,500)\b1\i1\c&H00FF00&}}{base_text}{{\b0\i0\c&HFFFFFF&}} with {{\t(0,1000,\frz360)}}animation{{\t(1000,2000,\frz0)}}"
270                )
271            }
272            ComplexityLevel::Extreme => {
273                format!(
274                    r"{{\pos(100,200)\move(100,200,500,400)\fad(300,300)\t(0,500,\fscx120\fscy120)\t(500,1000,\fscx100\fscy100)\b1\i1\u1\s1\bord2\shad2\c&H00FF00&\3c&H0000FF&\4c&H000000&\alpha&H00\3a&H80}}{base_text}{{\b0\i0\u0\s0\r}} {{\k50}}with {{\k30}}karaoke {{\k40}}timing {{\k60}}and {{\k45}}complex {{\k35}}animations"
275                )
276            }
277            ComplexityLevel::AnimeRealistic => {
278                Self::generate_anime_dialogue(event_index, &base_text)
279            }
280            ComplexityLevel::MovieRealistic => {
281                Self::generate_movie_dialogue(event_index, &base_text)
282            }
283            ComplexityLevel::KaraokeRealistic => {
284                Self::generate_karaoke_dialogue(event_index, &base_text)
285            }
286            ComplexityLevel::SignRealistic => Self::generate_sign_dialogue(event_index, &base_text),
287            ComplexityLevel::EducationalRealistic => {
288                Self::generate_educational_dialogue(event_index, &base_text)
289            }
290        }
291    }
292
293    /// Generate anime-style dialogue with heavy effects
294    fn generate_anime_dialogue(event_index: usize, base_text: &str) -> String {
295        let patterns = [
296            // Character speaking with glow effect
297            format!(
298                r"{{\an8\pos(960,80)\fad(250,250)\bord3\shad0\c&H00FFFFFF&\3c&H00FF8C00&}}{base_text}"
299            ),
300            // Thought bubble with transparency
301            format!(
302                r"{{\an5\pos(960,540)\fad(500,500)\alpha&H80&\bord2\c&H00E6E6FA&\3c&H00483D8B&}}{base_text}"
303            ),
304            // Dramatic effect with color change
305            format!(
306                r"{{\an2\pos(960,980)\fad(300,800)\b1\bord4\shad3\c&H0000FFFF&\3c&H000000FF&\4c&H00000000&\t(0,2000,\c&H00FF0000&)}}{base_text}"
307            ),
308            // Side character with positioning
309            format!(
310                r"{{\an7\pos(200,400)\fad(200,200)\bord2\c&H00FFFFFF&\3c&H00800080&}}{base_text}"
311            ),
312        ];
313        patterns[event_index % patterns.len()].clone()
314    }
315
316    /// Generate movie-style dialogue (simple and clean)
317    fn generate_movie_dialogue(event_index: usize, base_text: &str) -> String {
318        let patterns = [
319            // Standard dialogue
320            base_text.to_string(),
321            // Italic for emphasis
322            format!(r"{{\i1}}{base_text}{{\i0}}"),
323            // Bold for shouting
324            format!(r"{{\b1}}{base_text}{{\b0}}"),
325            // Different speaker position
326            format!(r"{{\an8}}{base_text}"),
327        ];
328        patterns[event_index % patterns.len()].clone()
329    }
330
331    /// Generate karaoke-style dialogue with timing
332    fn generate_karaoke_dialogue(event_index: usize, base_text: &str) -> String {
333        let words: Vec<&str> = base_text.split_whitespace().collect();
334        let mut karaoke_text = String::new();
335
336        // Add base styling
337        karaoke_text.push_str(
338            r"{\an5\pos(960,540)\fad(200,200)\b1\bord2\shad1\c&H00FFFFFF&\3c&H00FF6347&}",
339        );
340
341        // Add karaoke timing for each word
342        for (i, word) in words.iter().enumerate() {
343            let timing = 50 + (i * 30); // Varying timing
344            write!(karaoke_text, r"{{\k{timing}}}{word} ").unwrap();
345        }
346
347        // Add final effect
348        if event_index % 3 == 0 {
349            karaoke_text.push_str(r"{\t(2000,3000,\fscx120\fscy120\alpha&HFF&)}");
350        }
351
352        karaoke_text
353    }
354
355    /// Generate sign translation dialogue with positioning
356    fn generate_sign_dialogue(event_index: usize, base_text: &str) -> String {
357        let positions = [
358            // Top signs
359            (r"{\an8\pos(960,100)", "RESTAURANT"),
360            (r"{\an9\pos(1700,150)", "EXIT"),
361            (r"{\an7\pos(220,120)", "HOTEL"),
362            // Screen text
363            (r"{\an5\pos(960,540)", "NEWS FLASH"),
364            // Bottom signs
365            (r"{\an2\pos(960,950)", "SUBWAY"),
366            // Side signs
367            (r"{\an4\pos(100,540)", "STORE"),
368        ];
369
370        let (pos_tag, sign_type) = &positions[event_index % positions.len()];
371        let sign_text = if base_text.contains("number") {
372            format!(
373                "{sign_type}: {}",
374                base_text.replace("dialogue line", "sign")
375            )
376        } else {
377            (*sign_type).to_string()
378        };
379
380        format!(
381            r"{pos_tag}\fad(500,500)\bord3\shad2\c&H00000000&\3c&H00FFFFFF&\fn{{Arial}}\fs36}}{sign_text}"
382        )
383    }
384
385    /// Generate educational content dialogue
386    fn generate_educational_dialogue(event_index: usize, base_text: &str) -> String {
387        let patterns = [
388            // Main content
389            format!(
390                r"{{\an2\pos(960,900)\fad(200,200)\bord1\c&H00FFFFFF&}}{base_text} - This explains the concept in detail with proper formatting."
391            ),
392            // Question format
393            format!(
394                r"{{\an8\pos(960,150)\fad(200,200)\b1\c&H0000FFFF&}}Question {}: {base_text}",
395                event_index + 1
396            ),
397            // Answer format
398            format!(r"{{\an7\pos(100,400)\fad(200,200)\i1\c&H0000FF00&}}Answer: {base_text}"),
399            // Definition
400            format!(
401                r"{{\an5\pos(960,540)\fad(200,200)\bord2\c&H00FFFFFF&\3c&H000080FF&}}Definition: {base_text}"
402            ),
403            // Example
404            format!(r"{{\an1\pos(100,900)\fad(200,200)\c&H00FFFF00&}}Example: {base_text}"),
405            // Summary
406            format!(r"{{\an9\pos(1700,100)\fad(200,200)\b1\c&H00FF8000&}}Summary: {base_text}"),
407        ];
408        patterns[event_index % patterns.len()].clone()
409    }
410}
411
412/// Create a test event with the given parameters
413///
414/// This is a convenience function for creating `Event` instances in tests
415/// and benchmarks without having to specify all the fields manually.
416///
417/// # Examples
418///
419/// ```
420/// use ass_core::utils::benchmark_generators::create_test_event;
421///
422/// let event = create_test_event("0:00:00.00", "0:00:05.00", "Hello world");
423/// assert_eq!(event.text, "Hello world");
424/// ```
425#[must_use]
426pub const fn create_test_event<'a>(start: &'a str, end: &'a str, text: &'a str) -> Event<'a> {
427    Event {
428        event_type: EventType::Dialogue,
429        layer: "0",
430        start,
431        end,
432        style: "Default",
433        name: "",
434        margin_l: "0",
435        margin_r: "0",
436        margin_v: "0",
437        margin_t: None,
438        margin_b: None,
439        effect: "",
440        text,
441        span: Span::new(0, 0, 0, 0),
442    }
443}
444
445/// Generate script with intentional issues for linting benchmarks
446///
447/// Creates an ASS script containing various problematic patterns that
448/// linting rules should detect, such as empty tags, unknown tags, and
449/// performance-heavy animations.
450///
451/// # Arguments
452///
453/// * `event_count` - Number of events to generate in the script
454///
455/// # Returns
456///
457/// A complete ASS script string with intentional issues for testing
458/// linting performance and accuracy.
459#[must_use]
460pub fn generate_script_with_issues(event_count: usize) -> String {
461    let mut script = String::from(
462        "[Script Info]\n\
463        Title: Test Script\n\n\
464        [V4+ Styles]\n\
465        Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n\
466        Style: Default,Arial,20,&H00FFFFFF&,&H000000FF&,&H00000000&,&H00000000&,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n\
467        [Events]\n\
468        Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
469    );
470
471    for i in 0..event_count {
472        let start_time = format!("0:{:02}:{:02}.00", i / 60, i % 60);
473        let end_time = format!("0:{:02}:{:02}.50", i / 60, i % 60);
474
475        // Add some problematic content every 10th event
476        let text_string;
477        let text = if i % 10 == 0 {
478            r"Text with {\} empty tag and {\invalidtag} unknown tag"
479        } else if i % 7 == 0 {
480            // Very complex animation that might cause performance issues
481            r"{\pos(100,200)\move(100,200,500,400,0,5000)\t(0,1000,\frz360)\t(1000,2000,\fscx200\fscy200)\t(2000,3000,\alpha&HFF&)\t(3000,4000,\alpha&H00&)\t(4000,5000,\c&HFF0000&)}Performance heavy animation"
482        } else {
483            let line_num = i + 1;
484            text_string = format!("Normal dialogue line {line_num}");
485            &text_string
486        };
487
488        writeln!(
489            script,
490            "Dialogue: 0,{start_time},{end_time},Default,Speaker,0,0,0,,{text}"
491        )
492        .unwrap();
493    }
494
495    script
496}
497
498/// Generate script with overlapping events for timing analysis benchmarks
499///
500/// Creates an ASS script where events have overlapping time ranges,
501/// useful for testing overlap detection algorithms and timing analysis
502/// performance.
503///
504/// # Arguments
505///
506/// * `event_count` - Number of overlapping events to generate
507///
508/// # Returns
509///
510/// A complete ASS script string with overlapping dialogue events.
511#[must_use]
512pub fn generate_overlapping_script(event_count: usize) -> String {
513    let mut script = String::from(
514        r"[V4+ Styles]
515Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
516Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1
517
518[Events]
519Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
520",
521    );
522
523    for i in 0..event_count {
524        // Create overlapping events: each event overlaps with several others
525        let start_time = i * 2; // 2 second intervals
526        let end_time = start_time + 5; // 5 second duration (overlaps next 2-3 events)
527        writeln!(
528            &mut script,
529            "Dialogue: 0,0:{:02}:{:02}.00,0:{:02}:{:02}.00,Default,,0,0,0,,Event {} text",
530            start_time / 60,
531            start_time % 60,
532            end_time / 60,
533            end_time % 60,
534            i
535        )
536        .unwrap();
537    }
538
539    script
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    #[test]
547    fn script_generator_simple() {
548        let generator = ScriptGenerator::simple(5);
549        assert_eq!(generator.events_count, 5);
550        assert_eq!(generator.styles_count, 1);
551        assert!(matches!(
552            generator.complexity_level,
553            ComplexityLevel::Simple
554        ));
555
556        let script = generator.generate();
557        assert!(script.contains("[Script Info]"));
558        assert!(script.contains("[V4+ Styles]"));
559        assert!(script.contains("[Events]"));
560        assert!(script.contains("Simple Benchmark Script"));
561    }
562
563    #[test]
564    fn script_generator_moderate() {
565        let generator = ScriptGenerator::moderate(3);
566        assert_eq!(generator.events_count, 3);
567        assert_eq!(generator.styles_count, 5);
568        assert!(matches!(
569            generator.complexity_level,
570            ComplexityLevel::Moderate
571        ));
572    }
573
574    #[test]
575    fn script_generator_complex() {
576        let generator = ScriptGenerator::complex(2);
577        assert_eq!(generator.events_count, 2);
578        assert_eq!(generator.styles_count, 10);
579        assert!(matches!(
580            generator.complexity_level,
581            ComplexityLevel::Complex
582        ));
583    }
584
585    #[test]
586    fn script_generator_extreme() {
587        let generator = ScriptGenerator::extreme(1);
588        assert_eq!(generator.events_count, 1);
589        assert_eq!(generator.styles_count, 20);
590        assert!(matches!(
591            generator.complexity_level,
592            ComplexityLevel::Extreme
593        ));
594    }
595
596    #[test]
597    fn format_time_zero() {
598        assert_eq!(ScriptGenerator::format_time(0), "0:00:00.00");
599    }
600
601    #[test]
602    fn format_time_basic() {
603        assert_eq!(ScriptGenerator::format_time(6150), "0:01:01.50");
604    }
605
606    #[test]
607    fn format_time_hours() {
608        assert_eq!(ScriptGenerator::format_time(360_000), "1:00:00.00");
609    }
610
611    #[test]
612    fn create_test_event_basic() {
613        let event = create_test_event("0:00:00.00", "0:00:05.00", "Test text");
614        assert_eq!(event.start, "0:00:00.00");
615        assert_eq!(event.end, "0:00:05.00");
616        assert_eq!(event.text, "Test text");
617        assert_eq!(event.style, "Default");
618        assert!(matches!(event.event_type, EventType::Dialogue));
619    }
620
621    #[test]
622    fn generate_script_with_issues_basic() {
623        let script = generate_script_with_issues(5);
624        assert!(script.contains("[Script Info]"));
625        assert!(script.contains("[V4+ Styles]"));
626        assert!(script.contains("[Events]"));
627        assert!(script.contains("Dialogue:"));
628    }
629
630    #[test]
631    fn generate_script_with_issues_contains_problems() {
632        let script = generate_script_with_issues(20);
633        // Should contain some problematic content
634        assert!(script.lines().count() > 10);
635        // At least one event should have issues (every 10th event)
636        assert!(script.contains("empty tag") || script.contains("unknown tag"));
637    }
638
639    #[test]
640    fn generate_overlapping_script_basic() {
641        let script = generate_overlapping_script(3);
642        assert!(script.contains("[V4+ Styles]"));
643        assert!(script.contains("[Events]"));
644        assert!(script.contains("Event 0 text"));
645        assert!(script.contains("Event 1 text"));
646        assert!(script.contains("Event 2 text"));
647    }
648
649    #[test]
650    fn generate_overlapping_script_timing() {
651        let script = generate_overlapping_script(2);
652        // First event: 0:00:00.00 to 0:00:05.00
653        // Second event: 0:00:02.00 to 0:00:07.00 (overlaps with first)
654        assert!(script.contains("0:00:00.00"));
655        assert!(script.contains("0:00:05.00"));
656        assert!(script.contains("0:00:02.00"));
657        assert!(script.contains("0:00:07.00"));
658    }
659
660    #[test]
661    fn dialogue_text_complexity_simple() {
662        let generator = ScriptGenerator::simple(1);
663        let text = generator.generate_dialogue_text(0);
664        assert_eq!(text, "This is dialogue line number 1");
665    }
666
667    #[test]
668    fn dialogue_text_complexity_moderate() {
669        let generator = ScriptGenerator::moderate(1);
670        let text = generator.generate_dialogue_text(0);
671        assert!(text.contains(r"{\b1}"));
672        assert!(text.contains(r"{\i1}"));
673        assert!(text.contains("This is dialogue line number 1"));
674    }
675
676    #[test]
677    fn dialogue_text_complexity_complex() {
678        let generator = ScriptGenerator::complex(1);
679        let text = generator.generate_dialogue_text(0);
680        assert!(text.contains(r"{\pos("));
681        assert!(text.contains(r"{\t("));
682        assert!(text.contains("animation"));
683    }
684
685    #[test]
686    fn dialogue_text_complexity_extreme() {
687        let generator = ScriptGenerator::extreme(1);
688        let text = generator.generate_dialogue_text(0);
689        assert!(text.contains(r"{\k"));
690        assert!(text.contains("karaoke"));
691        assert!(text.contains("animations"));
692    }
693
694    #[test]
695    fn anime_realistic_generator() {
696        let generator = ScriptGenerator::anime_realistic(5);
697        assert_eq!(generator.events_count, 5);
698        assert_eq!(generator.styles_count, 15);
699        assert!(matches!(
700            generator.complexity_level,
701            ComplexityLevel::AnimeRealistic
702        ));
703
704        let script = generator.generate();
705        assert!(script.contains("Anime Subtitles"));
706    }
707
708    #[test]
709    fn movie_realistic_generator() {
710        let generator = ScriptGenerator::movie_realistic(3);
711        assert_eq!(generator.events_count, 3);
712        assert_eq!(generator.styles_count, 3);
713        assert!(matches!(
714            generator.complexity_level,
715            ComplexityLevel::MovieRealistic
716        ));
717    }
718
719    #[test]
720    fn karaoke_realistic_generator() {
721        let generator = ScriptGenerator::karaoke_realistic(2);
722        assert_eq!(generator.events_count, 2);
723        assert_eq!(generator.styles_count, 8);
724        assert!(matches!(
725            generator.complexity_level,
726            ComplexityLevel::KaraokeRealistic
727        ));
728
729        let text = generator.generate_dialogue_text(0);
730        assert!(text.contains(r"{\k"));
731    }
732
733    #[test]
734    fn sign_realistic_generator() {
735        let generator = ScriptGenerator::sign_realistic(4);
736        assert_eq!(generator.events_count, 4);
737        assert_eq!(generator.styles_count, 12);
738        assert!(matches!(
739            generator.complexity_level,
740            ComplexityLevel::SignRealistic
741        ));
742
743        let text = generator.generate_dialogue_text(0);
744        assert!(text.contains(r"{\pos(") || text.contains(r"{\an"));
745    }
746
747    #[test]
748    fn educational_realistic_generator() {
749        let generator = ScriptGenerator::educational_realistic(6);
750        assert_eq!(generator.events_count, 6);
751        assert_eq!(generator.styles_count, 6);
752        assert!(matches!(
753            generator.complexity_level,
754            ComplexityLevel::EducationalRealistic
755        ));
756
757        let text = generator.generate_dialogue_text(1);
758        assert!(
759            text.contains("Question") || text.contains("Answer") || text.contains("Definition")
760        );
761    }
762
763    #[test]
764    fn script_generator_generate_has_correct_event_count() {
765        let generator = ScriptGenerator::simple(3);
766        let script = generator.generate();
767        assert_eq!(
768            script
769                .lines()
770                .filter(|line| line.starts_with("Dialogue:"))
771                .count(),
772            3
773        );
774    }
775
776    #[test]
777    fn script_generator_generate_has_correct_style_count() {
778        let generator = ScriptGenerator::moderate(1); // 5 styles
779        let script = generator.generate();
780        assert_eq!(
781            script
782                .lines()
783                .filter(|line| line.starts_with("Style:"))
784                .count(),
785            5
786        );
787    }
788}