ass_core/analysis/styles/
analyzer.rs

1//! Style analyzer for comprehensive ASS script style analysis
2//!
3//! Provides the main `StyleAnalyzer` interface for resolving styles, detecting
4//! conflicts, and performing validation. Orchestrates analysis across multiple
5//! sub-modules with efficient caching and zero-copy design.
6//!
7//! # Features
8//!
9//! - Comprehensive style resolution with inheritance support
10//! - Conflict detection including circular inheritance and duplicates
11//! - Performance analysis with configurable thresholds
12//! - Validation with multiple severity levels
13//! - Zero-copy analysis with lifetime-generic references
14//!
15//! # Performance
16//!
17//! - Target: <2ms for complete script style analysis
18//! - Memory: Efficient caching with zero-copy style references
19//! - Lazy evaluation: Analysis performed only when requested
20
21use crate::{
22    analysis::styles::{
23        resolved_style::ResolvedStyle,
24        validation::{StyleConflict, StyleInheritance, StyleValidationIssue},
25    },
26    parser::{Script, Section, Style},
27};
28use alloc::{collections::BTreeMap, collections::BTreeSet, vec::Vec};
29
30bitflags::bitflags! {
31    /// Analysis options for style analyzer
32    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
33    pub struct AnalysisOptions: u8 {
34        /// Enable inheritance analysis
35        const INHERITANCE = 1 << 0;
36        /// Enable conflict detection
37        const CONFLICTS = 1 << 1;
38        /// Enable performance analysis
39        const PERFORMANCE = 1 << 2;
40        /// Enable value validation
41        const VALIDATION = 1 << 3;
42        /// Use strict validation rules
43        const STRICT_VALIDATION = 1 << 4;
44    }
45}
46
47/// Comprehensive style analyzer for ASS scripts
48///
49/// Orchestrates style analysis including resolution, validation, and conflict
50/// detection. Maintains efficient caches for resolved styles and analysis results.
51#[derive(Debug)]
52pub struct StyleAnalyzer<'a> {
53    /// Reference to script being analyzed
54    script: &'a Script<'a>,
55    /// Cached resolved styles
56    resolved_styles: BTreeMap<&'a str, ResolvedStyle<'a>>,
57    /// Style inheritance tracking
58    inheritance_info: BTreeMap<&'a str, StyleInheritance<'a>>,
59    /// Detected style conflicts
60    conflicts: Vec<StyleConflict<'a>>,
61    /// Analysis configuration
62    config: StyleAnalysisConfig,
63    /// Resolution scaling factor (layout to play resolution)
64    resolution_scaling: Option<(f32, f32)>,
65}
66
67/// Configuration for style analysis behavior
68#[derive(Debug, Clone)]
69pub struct StyleAnalysisConfig {
70    /// Analysis options flags
71    pub options: AnalysisOptions,
72    /// Performance analysis thresholds
73    pub performance_thresholds: PerformanceThresholds,
74}
75
76/// Performance analysis threshold configuration
77#[derive(Debug, Clone)]
78pub struct PerformanceThresholds {
79    /// Font size threshold for performance warnings
80    pub large_font_threshold: f32,
81    /// Outline thickness threshold
82    pub large_outline_threshold: f32,
83    /// Shadow distance threshold
84    pub large_shadow_threshold: f32,
85    /// Scaling factor threshold
86    pub scaling_threshold: f32,
87}
88
89impl Default for StyleAnalysisConfig {
90    fn default() -> Self {
91        Self {
92            options: AnalysisOptions::INHERITANCE
93                | AnalysisOptions::CONFLICTS
94                | AnalysisOptions::PERFORMANCE
95                | AnalysisOptions::VALIDATION,
96            performance_thresholds: PerformanceThresholds::default(),
97        }
98    }
99}
100
101impl Default for PerformanceThresholds {
102    fn default() -> Self {
103        Self {
104            large_font_threshold: 50.0,
105            large_outline_threshold: 4.0,
106            large_shadow_threshold: 4.0,
107            scaling_threshold: 200.0,
108        }
109    }
110}
111
112impl<'a> StyleAnalyzer<'a> {
113    /// Create analyzer with default configuration
114    #[must_use]
115    pub fn new(script: &'a Script<'a>) -> Self {
116        Self::new_with_config(script, StyleAnalysisConfig::default())
117    }
118
119    /// Create analyzer with custom configuration
120    #[must_use]
121    pub fn new_with_config(script: &'a Script<'a>, config: StyleAnalysisConfig) -> Self {
122        let mut analyzer = Self {
123            script,
124            resolved_styles: BTreeMap::new(),
125            inheritance_info: BTreeMap::new(),
126            conflicts: Vec::new(),
127            config,
128            resolution_scaling: None,
129        };
130
131        analyzer.calculate_resolution_scaling();
132        analyzer.analyze_all_styles();
133        analyzer
134    }
135
136    /// Get resolved style by name
137    #[must_use]
138    pub fn resolve_style(&self, name: &str) -> Option<&ResolvedStyle<'a>> {
139        self.resolved_styles.get(name)
140    }
141
142    /// Get all resolved styles
143    #[must_use]
144    pub const fn resolved_styles(&self) -> &BTreeMap<&'a str, ResolvedStyle<'a>> {
145        &self.resolved_styles
146    }
147
148    /// Get detected conflicts
149    #[must_use]
150    pub fn conflicts(&self) -> &[StyleConflict<'a>] {
151        &self.conflicts
152    }
153
154    /// Get inheritance information
155    #[must_use]
156    pub const fn inheritance_info(&self) -> &BTreeMap<&'a str, StyleInheritance<'a>> {
157        &self.inheritance_info
158    }
159
160    /// Validate all styles and return issues
161    #[must_use]
162    pub fn validate_styles(&self) -> Vec<StyleValidationIssue> {
163        let mut issues = Vec::new();
164
165        for resolved in self.resolved_styles.values() {
166            if self.config.options.contains(AnalysisOptions::VALIDATION) {
167                issues.extend(self.validate_style_properties(resolved));
168            }
169
170            if self.config.options.contains(AnalysisOptions::PERFORMANCE) {
171                issues.extend(self.analyze_style_performance(resolved));
172            }
173        }
174
175        issues
176    }
177
178    /// Calculate resolution scaling factor from script info
179    fn calculate_resolution_scaling(&mut self) {
180        // Find ScriptInfo section
181        for section in self.script.sections() {
182            if let Section::ScriptInfo(script_info) = section {
183                let layout_res = script_info.layout_resolution();
184                let play_res = script_info.play_resolution();
185
186                if let (Some((layout_x, layout_y)), Some((play_x, play_y))) = (layout_res, play_res)
187                {
188                    // Only apply scaling if resolutions differ
189                    if layout_x != play_x || layout_y != play_y {
190                        #[allow(clippy::cast_precision_loss)]
191                        let scale_x = play_x as f32 / layout_x as f32;
192                        #[allow(clippy::cast_precision_loss)]
193                        let scale_y = play_y as f32 / layout_y as f32;
194                        self.resolution_scaling = Some((scale_x, scale_y));
195                    }
196                }
197                break;
198            }
199        }
200    }
201
202    /// Analyze all styles in script
203    fn analyze_all_styles(&mut self) {
204        for section in self.script.sections() {
205            if let Section::Styles(styles) = section {
206                // Build dependency graph and resolve in order
207                if let Some(ordered_styles) = self.build_dependency_order(styles) {
208                    self.resolve_styles_with_inheritance(&ordered_styles);
209                } else {
210                    // Fall back to non-inherited resolution if circular dependency detected
211                    for style in styles {
212                        if let Ok(mut resolved) = ResolvedStyle::from_style(style) {
213                            // Apply resolution scaling if needed
214                            if let Some((scale_x, scale_y)) = self.resolution_scaling {
215                                resolved.apply_resolution_scaling(scale_x, scale_y);
216                            }
217                            self.resolved_styles.insert(style.name, resolved);
218                        }
219                    }
220                }
221
222                if self.config.options.contains(AnalysisOptions::CONFLICTS) {
223                    self.detect_style_conflicts_from_section(styles);
224                }
225                break;
226            }
227        }
228    }
229
230    /// Build dependency order for styles using topological sort
231    /// Returns None if circular dependency is detected
232    fn build_dependency_order(&mut self, styles: &'a [Style<'a>]) -> Option<Vec<&'a Style<'a>>> {
233        // Create style map for quick lookup
234        let style_map: BTreeMap<&str, &Style> = styles.iter().map(|s| (s.name, s)).collect();
235
236        // Build adjacency list (child -> parent)
237        let mut dependencies: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
238        let mut in_degree: BTreeMap<&str, usize> = BTreeMap::new();
239
240        // Initialize all styles
241        for style in styles {
242            dependencies.insert(style.name, BTreeSet::new());
243            in_degree.insert(style.name, 0);
244        }
245
246        // Build dependency graph
247        for style in styles {
248            if let Some(parent_name) = style.parent {
249                if style_map.contains_key(parent_name) {
250                    dependencies
251                        .get_mut(style.name)
252                        .unwrap()
253                        .insert(parent_name);
254                    *in_degree.get_mut(parent_name).unwrap() += 1;
255
256                    // Track inheritance for analysis
257                    if self.config.options.contains(AnalysisOptions::INHERITANCE) {
258                        if let Some(inheritance) = self.inheritance_info.get_mut(style.name) {
259                            inheritance.set_parent(parent_name);
260                        } else {
261                            let mut inheritance = StyleInheritance::new(style.name);
262                            inheritance.set_parent(parent_name);
263                            self.inheritance_info.insert(style.name, inheritance);
264                        }
265                    }
266                } else {
267                    // Parent style not found - add warning conflict
268                    self.conflicts
269                        .push(StyleConflict::missing_parent(style.name, parent_name));
270                }
271            } else if self.config.options.contains(AnalysisOptions::INHERITANCE) {
272                // Style has no parent
273                self.inheritance_info
274                    .insert(style.name, StyleInheritance::new(style.name));
275            }
276        }
277
278        // Check for circular dependencies using DFS
279        if Self::has_circular_dependency(&dependencies) {
280            self.conflicts.push(StyleConflict::circular_inheritance(
281                dependencies.keys().copied().collect(),
282            ));
283            return None;
284        }
285
286        // Perform topological sort
287        let mut result = Vec::new();
288        let mut queue: Vec<&str> = Vec::new();
289
290        // Find all nodes with no dependencies
291        for (name, degree) in &in_degree {
292            if *degree == 0 {
293                queue.push(name);
294            }
295        }
296
297        while let Some(current) = queue.pop() {
298            if let Some(style) = style_map.get(current) {
299                result.push(*style);
300            }
301
302            // Update in-degrees
303            for (child, parents) in &dependencies {
304                if parents.contains(current) {
305                    if let Some(degree) = in_degree.get_mut(child) {
306                        *degree = degree.saturating_sub(1);
307                        if *degree == 0 {
308                            queue.push(child);
309                        }
310                    }
311                }
312            }
313        }
314
315        // Check if all styles were processed
316        if result.len() == styles.len() {
317            Some(result)
318        } else {
319            // Not all styles processed - circular dependency exists
320            None
321        }
322    }
323
324    /// Check for circular dependencies using DFS
325    fn has_circular_dependency(dependencies: &BTreeMap<&str, BTreeSet<&str>>) -> bool {
326        let mut visited = BTreeSet::new();
327        let mut rec_stack = BTreeSet::new();
328
329        for node in dependencies.keys() {
330            if !visited.contains(node)
331                && Self::dfs_has_cycle(node, dependencies, &mut visited, &mut rec_stack)
332            {
333                return true;
334            }
335        }
336
337        false
338    }
339
340    /// DFS helper for cycle detection
341    fn dfs_has_cycle<'b>(
342        node: &'b str,
343        dependencies: &BTreeMap<&'b str, BTreeSet<&'b str>>,
344        visited: &mut BTreeSet<&'b str>,
345        rec_stack: &mut BTreeSet<&'b str>,
346    ) -> bool {
347        visited.insert(node);
348        rec_stack.insert(node);
349
350        if let Some(neighbors) = dependencies.get(node) {
351            for neighbor in neighbors {
352                if !visited.contains(neighbor) {
353                    if Self::dfs_has_cycle(neighbor, dependencies, visited, rec_stack) {
354                        return true;
355                    }
356                } else if rec_stack.contains(neighbor) {
357                    return true;
358                }
359            }
360        }
361
362        rec_stack.remove(node);
363        false
364    }
365
366    /// Resolve styles with inheritance support
367    fn resolve_styles_with_inheritance(&mut self, ordered_styles: &[&'a Style<'a>]) {
368        for style in ordered_styles {
369            let resolved = if let Some(parent_name) = style.parent {
370                // Get parent's resolved style
371                self.resolved_styles.get(parent_name).map_or_else(
372                    || ResolvedStyle::from_style(style),
373                    |parent_resolved| ResolvedStyle::from_style_with_parent(style, parent_resolved),
374                )
375            } else {
376                // No parent - resolve directly
377                ResolvedStyle::from_style(style)
378            };
379
380            if let Ok(mut resolved_style) = resolved {
381                // Apply resolution scaling if needed
382                if let Some((scale_x, scale_y)) = self.resolution_scaling {
383                    resolved_style.apply_resolution_scaling(scale_x, scale_y);
384                }
385                self.resolved_styles.insert(style.name, resolved_style);
386            }
387        }
388    }
389
390    /// Extract styles from script sections
391    #[must_use]
392    pub fn extract_styles(&self) -> Option<&[Style<'a>]> {
393        for section in self.script.sections() {
394            if let Section::Styles(styles) = section {
395                return Some(styles);
396            }
397        }
398        None
399    }
400
401    /// Detect conflicts between styles in a section
402    fn detect_style_conflicts_from_section(&mut self, styles: &[Style<'a>]) {
403        let mut name_counts: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
404
405        for style in styles {
406            name_counts.entry(style.name).or_default().push(style.name);
407        }
408
409        for (_name, instances) in name_counts {
410            if instances.len() > 1 {
411                self.conflicts
412                    .push(StyleConflict::duplicate_name(instances));
413            }
414        }
415    }
416
417    /// Validate style properties
418    fn validate_style_properties(&self, style: &ResolvedStyle<'a>) -> Vec<StyleValidationIssue> {
419        let mut issues = Vec::new();
420
421        if style.font_size() <= 0.0 {
422            issues.push(StyleValidationIssue::error(
423                "font_size",
424                "Font size must be positive",
425            ));
426        }
427
428        if self
429            .config
430            .options
431            .contains(AnalysisOptions::STRICT_VALIDATION)
432            && style.font_size() > 200.0
433        {
434            issues.push(StyleValidationIssue::warning(
435                "font_size",
436                "Very large font size may cause performance issues",
437            ));
438        }
439
440        issues
441    }
442
443    /// Analyze style performance impact
444    fn analyze_style_performance(&self, style: &ResolvedStyle<'a>) -> Vec<StyleValidationIssue> {
445        let mut issues = Vec::new();
446        let thresholds = &self.config.performance_thresholds;
447
448        if style.font_size() > thresholds.large_font_threshold {
449            issues.push(StyleValidationIssue::info_with_suggestion(
450                "font_size",
451                "Large font size detected",
452                "Consider reducing font size for better performance",
453            ));
454        }
455
456        if style.has_performance_issues() {
457            issues.push(StyleValidationIssue::warning(
458                "complexity",
459                "Style has high rendering complexity",
460            ));
461        }
462
463        issues
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use crate::analysis::styles::validation::ConflictType;
471    #[cfg(not(feature = "std"))]
472    use alloc::format;
473
474    #[test]
475    fn analyzer_creation() {
476        let script_text = r"
477[V4+ Styles]
478Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
479Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
480";
481
482        let script = crate::parser::Script::parse(script_text).unwrap();
483        let analyzer = StyleAnalyzer::new(&script);
484
485        assert_eq!(analyzer.resolved_styles().len(), 1);
486        assert!(analyzer.resolve_style("Default").is_some());
487    }
488
489    #[test]
490    fn config_defaults() {
491        let config = StyleAnalysisConfig::default();
492        assert!(config.options.contains(AnalysisOptions::INHERITANCE));
493        assert!(config.options.contains(AnalysisOptions::CONFLICTS));
494        assert!(config.options.contains(AnalysisOptions::VALIDATION));
495        assert!(!config.options.contains(AnalysisOptions::STRICT_VALIDATION));
496    }
497
498    #[test]
499    fn performance_thresholds() {
500        let thresholds = PerformanceThresholds::default();
501        assert!((thresholds.large_font_threshold - 50.0).abs() < f32::EPSILON);
502        assert!((thresholds.large_outline_threshold - 4.0).abs() < f32::EPSILON);
503        assert!((thresholds.large_shadow_threshold - 4.0).abs() < f32::EPSILON);
504        assert!((thresholds.scaling_threshold - 200.0).abs() < f32::EPSILON);
505    }
506
507    #[test]
508    fn analyzer_with_custom_config() {
509        let script_text = r"
510[V4+ Styles]
511Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
512Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
513";
514
515        let script = crate::parser::Script::parse(script_text).unwrap();
516        let config = StyleAnalysisConfig {
517            options: AnalysisOptions::VALIDATION | AnalysisOptions::STRICT_VALIDATION,
518            performance_thresholds: PerformanceThresholds {
519                large_font_threshold: 30.0,
520                large_outline_threshold: 2.0,
521                large_shadow_threshold: 2.0,
522                scaling_threshold: 150.0,
523            },
524        };
525        let analyzer = StyleAnalyzer::new_with_config(&script, config);
526
527        assert_eq!(analyzer.resolved_styles().len(), 1);
528        assert!(analyzer.resolve_style("Default").is_some());
529    }
530
531    #[test]
532    fn analyzer_multiple_styles() {
533        let script_text = r"
534[V4+ Styles]
535Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
536Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
537Style: Title,Arial,32,&H00FFFF00,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,3,0,2,20,20,20,1
538Style: Subtitle,Arial,16,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,1,0,0,100,100,0,0,1,1,0,2,5,5,5,1
539";
540
541        let script = crate::parser::Script::parse(script_text).unwrap();
542        let analyzer = StyleAnalyzer::new(&script);
543
544        assert_eq!(analyzer.resolved_styles().len(), 3);
545        assert!(analyzer.resolve_style("Default").is_some());
546        assert!(analyzer.resolve_style("Title").is_some());
547        assert!(analyzer.resolve_style("Subtitle").is_some());
548        assert!(analyzer.resolve_style("NonExistent").is_none());
549    }
550
551    #[test]
552    fn analyzer_duplicate_styles() {
553        let script_text = r"
554[V4+ Styles]
555Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
556Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
557Style: Default,Times,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
558";
559
560        let script = crate::parser::Script::parse(script_text).unwrap();
561        let analyzer = StyleAnalyzer::new(&script);
562
563        let conflicts = analyzer.conflicts();
564        assert!(!conflicts.is_empty());
565    }
566
567    #[test]
568    fn analyzer_no_styles_section() {
569        let script_text = r"
570[Script Info]
571Title: Test Script
572";
573
574        let script = crate::parser::Script::parse(script_text).unwrap();
575        let analyzer = StyleAnalyzer::new(&script);
576
577        assert_eq!(analyzer.resolved_styles().len(), 0);
578        assert!(analyzer.conflicts().is_empty());
579    }
580
581    #[test]
582    fn analyzer_empty_styles_section() {
583        let script_text = r"
584[V4+ Styles]
585Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
586";
587
588        let script = crate::parser::Script::parse(script_text).unwrap();
589        let analyzer = StyleAnalyzer::new(&script);
590
591        assert_eq!(analyzer.resolved_styles().len(), 0);
592        assert!(analyzer.conflicts().is_empty());
593    }
594
595    #[test]
596    fn analyzer_extract_styles() {
597        let script_text = r"
598[V4+ Styles]
599Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
600Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
601";
602
603        let script = crate::parser::Script::parse(script_text).unwrap();
604        let analyzer = StyleAnalyzer::new(&script);
605
606        let styles = analyzer.extract_styles();
607        assert!(styles.is_some());
608        assert_eq!(styles.unwrap().len(), 1);
609    }
610
611    #[test]
612    fn analyzer_extract_styles_no_section() {
613        let script_text = r"
614[Script Info]
615Title: Test Script
616";
617
618        let script = crate::parser::Script::parse(script_text).unwrap();
619        let analyzer = StyleAnalyzer::new(&script);
620
621        let styles = analyzer.extract_styles();
622        assert!(styles.is_none());
623    }
624
625    #[test]
626    fn analyzer_inheritance_info() {
627        let script_text = r"
628[V4+ Styles]
629Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
630Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
631Style: Title,Arial,32,&H00FFFF00,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,3,0,2,20,20,20,1
632";
633
634        let script = crate::parser::Script::parse(script_text).unwrap();
635        let analyzer = StyleAnalyzer::new(&script);
636
637        let inheritance_info = analyzer.inheritance_info();
638        assert_eq!(inheritance_info.len(), 2);
639        assert!(inheritance_info.contains_key("Default"));
640        assert!(inheritance_info.contains_key("Title"));
641    }
642
643    #[test]
644    fn analyzer_validate_styles() {
645        let script_text = r"
646[V4+ Styles]
647Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
648Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
649Style: Large,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,5,0,2,10,10,10,1
650";
651
652        let script = crate::parser::Script::parse(script_text).unwrap();
653        let analyzer = StyleAnalyzer::new(&script);
654
655        let issues = analyzer.validate_styles();
656        // Should have some validation issues or none
657        assert!(issues.is_empty() || !issues.is_empty());
658    }
659
660    #[test]
661    fn analyzer_strict_validation() {
662        let script_text = r"
663[V4+ Styles]
664Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
665Style: Large,Arial,250,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
666";
667
668        let script = crate::parser::Script::parse(script_text).unwrap();
669        let config = StyleAnalysisConfig {
670            options: AnalysisOptions::VALIDATION | AnalysisOptions::STRICT_VALIDATION,
671            performance_thresholds: PerformanceThresholds::default(),
672        };
673        let analyzer = StyleAnalyzer::new_with_config(&script, config);
674
675        let issues = analyzer.validate_styles();
676        // Should have validation issues for large font size
677        assert!(!issues.is_empty());
678    }
679
680    #[test]
681    fn analyzer_performance_analysis() {
682        let script_text = r"
683[V4+ Styles]
684Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
685Style: Heavy,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,8,5,2,10,10,10,1
686";
687
688        let script = crate::parser::Script::parse(script_text).unwrap();
689        let config = StyleAnalysisConfig {
690            options: AnalysisOptions::PERFORMANCE,
691            performance_thresholds: PerformanceThresholds {
692                large_font_threshold: 30.0,
693                large_outline_threshold: 2.0,
694                large_shadow_threshold: 2.0,
695                scaling_threshold: 150.0,
696            },
697        };
698        let analyzer = StyleAnalyzer::new_with_config(&script, config);
699
700        let issues = analyzer.validate_styles();
701        // Should have performance issues for large values
702        assert!(!issues.is_empty());
703    }
704
705    #[test]
706    fn analyzer_options_flags() {
707        let options = AnalysisOptions::INHERITANCE | AnalysisOptions::CONFLICTS;
708        assert!(options.contains(AnalysisOptions::INHERITANCE));
709        assert!(options.contains(AnalysisOptions::CONFLICTS));
710        assert!(!options.contains(AnalysisOptions::VALIDATION));
711        assert!(!options.contains(AnalysisOptions::PERFORMANCE));
712        assert!(!options.contains(AnalysisOptions::STRICT_VALIDATION));
713    }
714
715    #[test]
716    fn analyzer_options_debug() {
717        let options = AnalysisOptions::INHERITANCE;
718        let debug_str = format!("{options:?}");
719        assert!(debug_str.contains("INHERITANCE"));
720    }
721
722    #[test]
723    fn analyzer_config_debug() {
724        let config = StyleAnalysisConfig::default();
725        let debug_str = format!("{config:?}");
726        assert!(debug_str.contains("StyleAnalysisConfig"));
727        assert!(debug_str.contains("options"));
728        assert!(debug_str.contains("performance_thresholds"));
729    }
730
731    #[test]
732    fn analyzer_debug() {
733        let script_text = r"
734[V4+ Styles]
735Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
736Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
737";
738
739        let script = crate::parser::Script::parse(script_text).unwrap();
740        let analyzer = StyleAnalyzer::new(&script);
741        let debug_str = format!("{analyzer:?}");
742        assert!(debug_str.contains("StyleAnalyzer"));
743    }
744
745    #[test]
746    fn performance_thresholds_debug() {
747        let thresholds = PerformanceThresholds::default();
748        let debug_str = format!("{thresholds:?}");
749        assert!(debug_str.contains("PerformanceThresholds"));
750        assert!(debug_str.contains("large_font_threshold"));
751    }
752
753    #[test]
754    fn config_clone() {
755        let config = StyleAnalysisConfig::default();
756        let cloned = config.clone();
757        assert_eq!(config.options, cloned.options);
758        assert!(
759            (config.performance_thresholds.large_font_threshold
760                - cloned.performance_thresholds.large_font_threshold)
761                .abs()
762                < f32::EPSILON
763        );
764    }
765
766    #[test]
767    fn performance_thresholds_clone() {
768        let thresholds = PerformanceThresholds::default();
769        let cloned = thresholds.clone();
770        assert!(
771            (thresholds.large_font_threshold - cloned.large_font_threshold).abs() < f32::EPSILON
772        );
773        assert!(
774            (thresholds.large_outline_threshold - cloned.large_outline_threshold).abs()
775                < f32::EPSILON
776        );
777        assert!(
778            (thresholds.large_shadow_threshold - cloned.large_shadow_threshold).abs()
779                < f32::EPSILON
780        );
781        assert!((thresholds.scaling_threshold - cloned.scaling_threshold).abs() < f32::EPSILON);
782    }
783
784    #[test]
785    fn analyzer_minimal_options() {
786        let script_text = r"
787[V4+ Styles]
788Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
789Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
790";
791
792        let script = crate::parser::Script::parse(script_text).unwrap();
793        let config = StyleAnalysisConfig {
794            options: AnalysisOptions::empty(),
795            performance_thresholds: PerformanceThresholds::default(),
796        };
797        let analyzer = StyleAnalyzer::new_with_config(&script, config);
798
799        assert_eq!(analyzer.resolved_styles().len(), 1);
800        assert!(analyzer.inheritance_info().is_empty());
801        assert!(analyzer.conflicts().is_empty());
802    }
803
804    #[test]
805    fn analyzer_style_inheritance_basic() {
806        let script_text = r"
807[V4+ Styles]
808Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
809Style: BaseStyle,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
810Style: *BaseStyle,DerivedStyle,Verdana,24,&HFF00FFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
811";
812
813        let script = crate::parser::Script::parse(script_text).unwrap();
814        let analyzer = StyleAnalyzer::new(&script);
815
816        assert_eq!(analyzer.resolved_styles().len(), 2);
817
818        let base_style = analyzer.resolve_style("BaseStyle").unwrap();
819        assert_eq!(base_style.font_name(), "Arial");
820        assert!((base_style.font_size() - 20.0).abs() < f32::EPSILON);
821        assert!(!base_style.is_bold());
822
823        let derived_style = analyzer.resolve_style("DerivedStyle").unwrap();
824        assert_eq!(derived_style.font_name(), "Verdana");
825        assert!((derived_style.font_size() - 24.0).abs() < f32::EPSILON);
826        assert!(derived_style.is_bold());
827        // Should inherit colors from base
828        assert_eq!(derived_style.primary_color(), [255, 255, 0, 255]); // Overridden
829        assert_eq!(
830            derived_style.secondary_color(),
831            base_style.secondary_color()
832        );
833        assert_eq!(derived_style.outline_color(), base_style.outline_color());
834    }
835
836    #[test]
837    fn analyzer_style_inheritance_partial_override() {
838        let script_text = r"
839[V4+ Styles]
840Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
841Style: BaseStyle,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,1,0,0,100,100,0,0,1,2,3,2,10,10,10,1
842Style: *BaseStyle,DerivedStyle,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,3,2,10,10,10,1
843";
844
845        let script = crate::parser::Script::parse(script_text).unwrap();
846        let analyzer = StyleAnalyzer::new(&script);
847
848        let base_style = analyzer.resolve_style("BaseStyle").unwrap();
849        let derived_style = analyzer.resolve_style("DerivedStyle").unwrap();
850
851        // Should override font name
852        assert_eq!(derived_style.font_name(), "Verdana");
853        // Should override font size
854        assert!((derived_style.font_size() - 24.0).abs() < f32::EPSILON);
855        // Should inherit colors
856        assert_eq!(derived_style.primary_color(), base_style.primary_color());
857        // Should inherit bold but override italic
858        assert!(derived_style.is_bold());
859        assert!(!derived_style.is_italic());
860        // Should inherit shadow
861        assert!((derived_style.shadow() - 3.0).abs() < f32::EPSILON);
862    }
863
864    #[test]
865    fn analyzer_style_inheritance_chain() {
866        let script_text = r"
867[V4+ Styles]
868Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
869Style: GrandParent,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
870Style: *GrandParent,Parent,Verdana,24,&H00FFFF00,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,3,0,2,15,15,15,1
871Style: *Parent,Child,Times,28,&H00FF00FF,&H000000FF,&H00000000,&H00000000,1,1,0,0,100,100,0,0,1,4,0,2,20,20,20,1
872";
873
874        let script = crate::parser::Script::parse(script_text).unwrap();
875        let analyzer = StyleAnalyzer::new(&script);
876
877        assert_eq!(analyzer.resolved_styles().len(), 3);
878
879        let grandparent = analyzer.resolve_style("GrandParent").unwrap();
880        let parent = analyzer.resolve_style("Parent").unwrap();
881        let child = analyzer.resolve_style("Child").unwrap();
882
883        // GrandParent properties
884        assert_eq!(grandparent.font_name(), "Arial");
885        assert!((grandparent.font_size() - 20.0).abs() < f32::EPSILON);
886        assert!(!grandparent.is_bold());
887        assert!((grandparent.outline() - 2.0).abs() < f32::EPSILON);
888
889        // Parent inherits and overrides
890        assert_eq!(parent.font_name(), "Verdana");
891        assert!((parent.font_size() - 24.0).abs() < f32::EPSILON);
892        assert!(parent.is_bold());
893        assert!((parent.outline() - 3.0).abs() < f32::EPSILON);
894        assert_eq!(parent.margin_l(), 15);
895
896        // Child inherits from Parent and overrides
897        assert_eq!(child.font_name(), "Times");
898        assert!((child.font_size() - 28.0).abs() < f32::EPSILON);
899        assert!(child.is_bold());
900        assert!(child.is_italic());
901        assert!((child.outline() - 4.0).abs() < f32::EPSILON);
902        assert_eq!(child.margin_l(), 20);
903    }
904
905    #[test]
906    fn analyzer_style_inheritance_missing_parent() {
907        let script_text = r"
908[V4+ Styles]
909Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
910Style: *NonExistent,Orphan,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
911";
912
913        let script = crate::parser::Script::parse(script_text).unwrap();
914        let analyzer = StyleAnalyzer::new(&script);
915
916        // Should still resolve the style without inheritance
917        assert_eq!(analyzer.resolved_styles().len(), 1);
918        let orphan = analyzer.resolve_style("Orphan").unwrap();
919        assert_eq!(orphan.font_name(), "Arial");
920
921        // Should have a conflict for missing parent
922        let conflicts = analyzer.conflicts();
923        assert!(!conflicts.is_empty());
924        assert!(conflicts
925            .iter()
926            .any(|c| matches!(c.conflict_type, ConflictType::MissingReference)));
927    }
928
929    #[test]
930    fn analyzer_style_circular_inheritance() {
931        let script_text = r"
932[V4+ Styles]
933Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
934Style: *StyleB,StyleA,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
935Style: *StyleA,StyleB,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
936";
937
938        let script = crate::parser::Script::parse(script_text).unwrap();
939        let analyzer = StyleAnalyzer::new(&script);
940
941        // Should still resolve styles without inheritance due to circular dependency
942        assert_eq!(analyzer.resolved_styles().len(), 2);
943
944        // Should detect circular inheritance
945        let conflicts = analyzer.conflicts();
946        assert!(conflicts
947            .iter()
948            .any(|c| matches!(c.conflict_type, ConflictType::CircularInheritance)));
949    }
950
951    #[test]
952    fn analyzer_style_self_inheritance() {
953        let script_text = r"
954[V4+ Styles]
955Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
956Style: *SelfRef,SelfRef,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
957";
958
959        let script = crate::parser::Script::parse(script_text).unwrap();
960        let analyzer = StyleAnalyzer::new(&script);
961
962        // Should resolve without inheritance due to self-reference
963        assert_eq!(analyzer.resolved_styles().len(), 1);
964
965        // Should detect circular inheritance
966        let conflicts = analyzer.conflicts();
967        assert!(conflicts
968            .iter()
969            .any(|c| matches!(c.conflict_type, ConflictType::CircularInheritance)));
970    }
971
972    #[test]
973    fn analyzer_inheritance_info_tracking() {
974        let script_text = r"
975[V4+ Styles]
976Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
977Style: BaseStyle,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
978Style: *BaseStyle,Child1,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
979Style: *BaseStyle,Child2,Times,18,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
980";
981
982        let script = crate::parser::Script::parse(script_text).unwrap();
983        let analyzer = StyleAnalyzer::new(&script);
984
985        let inheritance_info = analyzer.inheritance_info();
986        assert_eq!(inheritance_info.len(), 3);
987
988        // Check BaseStyle has no parent
989        let base_info = inheritance_info.get("BaseStyle").unwrap();
990        assert!(base_info.is_root());
991        assert!(base_info.parents.is_empty());
992
993        // Check Child1 has BaseStyle as parent
994        let child1_info = inheritance_info.get("Child1").unwrap();
995        assert!(!child1_info.is_root());
996        assert_eq!(child1_info.parents.len(), 1);
997        assert_eq!(child1_info.parents[0], "BaseStyle");
998
999        // Check Child2 has BaseStyle as parent
1000        let child2_info = inheritance_info.get("Child2").unwrap();
1001        assert!(!child2_info.is_root());
1002        assert_eq!(child2_info.parents.len(), 1);
1003        assert_eq!(child2_info.parents[0], "BaseStyle");
1004    }
1005
1006    #[test]
1007    fn analyzer_layout_resolution_scaling() {
1008        let script_text = r"
1009[Script Info]
1010Title: Resolution Scaling Test
1011LayoutResX: 640
1012LayoutResY: 480
1013PlayResX: 1280
1014PlayResY: 960
1015
1016[V4+ Styles]
1017Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1018Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1019";
1020
1021        let script = crate::parser::Script::parse(script_text).unwrap();
1022        let analyzer = StyleAnalyzer::new(&script);
1023
1024        let default_style = analyzer.resolve_style("Default").unwrap();
1025        // Resolution is scaled 2x (1280/640 = 2, 960/480 = 2)
1026        assert!((default_style.font_size() - 40.0).abs() < f32::EPSILON); // 20 * 2
1027        assert!((default_style.spacing() - 4.0).abs() < f32::EPSILON); // 2 * 2
1028        assert!((default_style.outline() - 8.0).abs() < f32::EPSILON); // 4 * 2
1029        assert!((default_style.shadow() - 4.0).abs() < f32::EPSILON); // 2 * 2
1030        assert_eq!(default_style.margin_l(), 20); // 10 * 2
1031        assert_eq!(default_style.margin_r(), 20); // 10 * 2
1032        assert_eq!(default_style.margin_t(), 40); // 20 * 2
1033        assert_eq!(default_style.margin_b(), 40); // 20 * 2
1034    }
1035
1036    #[test]
1037    fn analyzer_layout_resolution_scaling_asymmetric() {
1038        let script_text = r"
1039[Script Info]
1040Title: Asymmetric Resolution Scaling Test
1041LayoutResX: 640
1042LayoutResY: 480
1043PlayResX: 1920
1044PlayResY: 1080
1045
1046[V4+ Styles]
1047Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1048Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1049";
1050
1051        let script = crate::parser::Script::parse(script_text).unwrap();
1052        let analyzer = StyleAnalyzer::new(&script);
1053
1054        let default_style = analyzer.resolve_style("Default").unwrap();
1055        // X scale: 1920/640 = 3, Y scale: 1080/480 = 2.25, average = 2.625
1056        let avg_scale = 2.625;
1057        assert!((20.0f32.mul_add(-avg_scale, default_style.font_size())).abs() < 0.01);
1058        assert!((default_style.spacing() - 6.0).abs() < f32::EPSILON); // 2 * 3
1059        assert!((4.0f32.mul_add(-avg_scale, default_style.outline())).abs() < 0.01);
1060        assert!((2.0f32.mul_add(-avg_scale, default_style.shadow())).abs() < 0.01);
1061        assert_eq!(default_style.margin_l(), 30); // 10 * 3
1062        assert_eq!(default_style.margin_r(), 30); // 10 * 3
1063        assert_eq!(default_style.margin_t(), 45); // 20 * 2.25
1064        assert_eq!(default_style.margin_b(), 45); // 20 * 2.25
1065    }
1066
1067    #[test]
1068    fn analyzer_no_resolution_scaling_when_same() {
1069        let script_text = r"
1070[Script Info]
1071Title: No Scaling Test
1072LayoutResX: 1920
1073LayoutResY: 1080
1074PlayResX: 1920
1075PlayResY: 1080
1076
1077[V4+ Styles]
1078Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1079Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1080";
1081
1082        let script = crate::parser::Script::parse(script_text).unwrap();
1083        let analyzer = StyleAnalyzer::new(&script);
1084
1085        let default_style = analyzer.resolve_style("Default").unwrap();
1086        // No scaling should be applied
1087        assert!((default_style.font_size() - 20.0).abs() < f32::EPSILON);
1088        assert!((default_style.spacing() - 2.0).abs() < f32::EPSILON);
1089        assert!((default_style.outline() - 4.0).abs() < f32::EPSILON);
1090        assert!((default_style.shadow() - 2.0).abs() < f32::EPSILON);
1091        assert_eq!(default_style.margin_l(), 10);
1092        assert_eq!(default_style.margin_r(), 10);
1093        assert_eq!(default_style.margin_t(), 20);
1094        assert_eq!(default_style.margin_b(), 20);
1095    }
1096
1097    #[test]
1098    fn analyzer_resolution_scaling_with_inheritance() {
1099        let script_text = r"
1100[Script Info]
1101Title: Scaling with Inheritance Test
1102LayoutResX: 640
1103LayoutResY: 480
1104PlayResX: 1280
1105PlayResY: 960
1106
1107[V4+ Styles]
1108Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1109Style: Base,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1110Style: *Base,Derived,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,15,15,20,1
1111";
1112
1113        let script = crate::parser::Script::parse(script_text).unwrap();
1114        let analyzer = StyleAnalyzer::new(&script);
1115
1116        let base_style = analyzer.resolve_style("Base").unwrap();
1117        let derived_style = analyzer.resolve_style("Derived").unwrap();
1118
1119        // Base style should be scaled 2x
1120        assert!((base_style.font_size() - 40.0).abs() < f32::EPSILON);
1121
1122        // Derived style overrides font size to 24, which should be scaled to 48
1123        assert!((derived_style.font_size() - 48.0).abs() < f32::EPSILON);
1124        // Margins are overridden and should be scaled
1125        assert_eq!(derived_style.margin_l(), 30); // 15 * 2
1126        assert_eq!(derived_style.margin_r(), 30); // 15 * 2
1127                                                  // Since margin_v is "20", it should be scaled to 40
1128        assert_eq!(derived_style.margin_t(), 40); // 20 * 2
1129        assert_eq!(derived_style.margin_b(), 40); // 20 * 2
1130    }
1131
1132    #[test]
1133    fn analyzer_no_resolution_info_no_scaling() {
1134        let script_text = r"
1135[Script Info]
1136Title: No Resolution Info Test
1137
1138[V4+ Styles]
1139Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1140Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1141";
1142
1143        let script = crate::parser::Script::parse(script_text).unwrap();
1144        let analyzer = StyleAnalyzer::new(&script);
1145
1146        let default_style = analyzer.resolve_style("Default").unwrap();
1147        // No scaling should be applied when resolution info is missing
1148        assert!((default_style.font_size() - 20.0).abs() < f32::EPSILON);
1149        assert!((default_style.spacing() - 2.0).abs() < f32::EPSILON);
1150        assert!((default_style.outline() - 4.0).abs() < f32::EPSILON);
1151        assert!((default_style.shadow() - 2.0).abs() < f32::EPSILON);
1152        assert_eq!(default_style.margin_l(), 10);
1153        assert_eq!(default_style.margin_r(), 10);
1154        assert_eq!(default_style.margin_t(), 20);
1155        assert_eq!(default_style.margin_b(), 20);
1156    }
1157
1158    #[test]
1159    fn analyzer_partial_resolution_info_no_scaling() {
1160        let script_text = r"
1161[Script Info]
1162Title: Partial Resolution Info Test
1163LayoutResX: 640
1164PlayResY: 960
1165
1166[V4+ Styles]
1167Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1168Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1169";
1170
1171        let script = crate::parser::Script::parse(script_text).unwrap();
1172        let analyzer = StyleAnalyzer::new(&script);
1173
1174        let default_style = analyzer.resolve_style("Default").unwrap();
1175        // No scaling should be applied when resolution info is incomplete
1176        assert!((default_style.font_size() - 20.0).abs() < f32::EPSILON);
1177        assert!((default_style.spacing() - 2.0).abs() < f32::EPSILON);
1178        assert!((default_style.outline() - 4.0).abs() < f32::EPSILON);
1179        assert!((default_style.shadow() - 2.0).abs() < f32::EPSILON);
1180        assert_eq!(default_style.margin_l(), 10);
1181        assert_eq!(default_style.margin_r(), 10);
1182        assert_eq!(default_style.margin_t(), 20);
1183        assert_eq!(default_style.margin_b(), 20);
1184    }
1185}