ass_core/analysis/styles/
resolved_style.rs

1//! Resolved style representation with computed values and metrics
2//!
3//! Provides the `ResolvedStyle` struct containing fully computed style properties
4//! after applying inheritance, overrides, and default fallbacks. Includes
5//! performance analysis and rendering complexity assessment.
6//!
7//! # Features
8//!
9//! - Zero-copy style name references to original definitions
10//! - Computed RGBA color values for efficient rendering
11//! - Performance complexity scoring (0-100 scale)
12//! - Font and layout property validation
13//! - Memory-efficient representation via packed fields
14//!
15//! # Performance
16//!
17//! - Target: <0.1ms per style resolution
18//! - Memory: ~200 bytes per resolved style
19//! - Zero allocations for style name references
20
21use crate::{parser::Style, utils::CoreError, Result};
22use alloc::{string::String, string::ToString};
23
24bitflags::bitflags! {
25    /// Text formatting options for resolved styles
26    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
27    pub struct TextFormatting: u8 {
28        /// Bold text formatting
29        const BOLD = 1 << 0;
30        /// Italic text formatting
31        const ITALIC = 1 << 1;
32        /// Underline text formatting
33        const UNDERLINE = 1 << 2;
34        /// Strike-through text formatting
35        const STRIKE_OUT = 1 << 3;
36    }
37}
38
39/// Fully resolved style with computed values and performance metrics
40///
41/// Contains effective style values after applying inheritance, overrides,
42/// and defaults. Optimized for rendering with pre-computed color values
43/// and complexity scoring for performance assessment.
44#[derive(Debug, Clone, PartialEq)]
45pub struct ResolvedStyle<'a> {
46    /// Original style name (zero-copy reference)
47    pub name: &'a str,
48    /// Resolved font family name
49    font_name: String,
50    /// Font size in points
51    font_size: f32,
52    /// Primary text color (RGBA)
53    primary_color: [u8; 4],
54    /// Secondary text color (RGBA)
55    secondary_color: [u8; 4],
56    /// Outline color (RGBA)
57    outline_color: [u8; 4],
58    /// Background color (RGBA)
59    back_color: [u8; 4],
60    /// Text formatting flags
61    formatting: TextFormatting,
62    /// Scaling factors (percentage)
63    /// Horizontal scaling factor
64    scale_x: f32,
65    /// Vertical scaling factor
66    scale_y: f32,
67    /// Character spacing
68    spacing: f32,
69    /// Text rotation angle
70    angle: f32,
71    /// Border style (`0=outline+drop_shadow`, `1=opaque_box`)
72    border_style: u8,
73    /// Outline thickness
74    outline: f32,
75    /// Shadow distance
76    shadow: f32,
77    /// Text alignment (1-9, numpad layout)
78    alignment: u8,
79    /// Margins in pixels
80    /// Left margin in pixels
81    margin_l: u16,
82    /// Right margin in pixels
83    margin_r: u16,
84    /// Top margin in pixels
85    margin_t: u16,
86    /// Bottom margin in pixels
87    margin_b: u16,
88    /// Text encoding
89    encoding: u8,
90    /// Rendering complexity score (0-100)
91    complexity_score: u8,
92}
93
94impl<'a> ResolvedStyle<'a> {
95    /// Create `ResolvedStyle` from base Style definition
96    ///
97    /// Resolves all style properties, validates values, and computes
98    /// performance metrics. Invalid values are replaced with defaults.
99    ///
100    /// # Arguments
101    ///
102    /// * `style` - Base style definition to resolve
103    ///
104    /// # Returns
105    ///
106    /// Fully resolved style with computed properties.
107    ///
108    /// # Example
109    ///
110    /// ```rust
111    /// # use ass_core::analysis::styles::resolved_style::ResolvedStyle;
112    /// # use ass_core::parser::Style;
113    /// let style = Style { name: "Default", fontname: "Arial", fontsize: "20", ..Default::default() };
114    /// let resolved = ResolvedStyle::from_style(&style)?;
115    /// assert_eq!(resolved.font_name(), "Arial");
116    /// assert_eq!(resolved.font_size(), 20.0);
117    /// # Ok::<(), Box<dyn std::error::Error>>(())
118    /// ```
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if style parsing fails or contains invalid values.
123    pub fn from_style(style: &'a Style<'a>) -> Result<Self> {
124        let font_name = if style.fontname.is_empty() {
125            "Arial".to_string()
126        } else {
127            style.fontname.to_string()
128        };
129
130        let font_size = parse_font_size(style.fontsize)?;
131        let primary_color = parse_color_with_default(style.primary_colour)?;
132        let secondary_color = parse_color_with_default(style.secondary_colour)?;
133        let outline_color = parse_color_with_default(style.outline_colour)?;
134        let back_color = parse_color_with_default(style.back_colour)?;
135
136        let mut formatting = TextFormatting::empty();
137        if parse_bool_flag(style.bold)? {
138            formatting |= TextFormatting::BOLD;
139        }
140        if parse_bool_flag(style.italic)? {
141            formatting |= TextFormatting::ITALIC;
142        }
143        if parse_bool_flag(style.underline)? {
144            formatting |= TextFormatting::UNDERLINE;
145        }
146        if parse_bool_flag(style.strikeout)? {
147            formatting |= TextFormatting::STRIKE_OUT;
148        }
149
150        let scale_x = parse_percentage(style.scale_x)?;
151        let scale_y = parse_percentage(style.scale_y)?;
152        let spacing = parse_float(style.spacing)?;
153        let angle = parse_float(style.angle)?;
154
155        let border_style = parse_u8(style.border_style)?;
156        let outline = parse_float(style.outline)?;
157        let shadow = parse_float(style.shadow)?;
158
159        let alignment = parse_u8(style.alignment)?;
160        let margin_l = parse_u16(style.margin_l)?;
161        let margin_r = parse_u16(style.margin_r)?;
162
163        // Handle v4+ vs v4++ margin formats
164        let (margin_t, margin_b) = if let (Some(t), Some(b)) = (style.margin_t, style.margin_b) {
165            // v4++ format with separate top/bottom margins
166            (parse_u16(t)?, parse_u16(b)?)
167        } else {
168            // v4+ format with single vertical margin
169            let margin_v = parse_u16(style.margin_v)?;
170            (margin_v, margin_v)
171        };
172
173        let encoding = parse_u8(style.encoding)?;
174
175        let resolved = Self {
176            name: style.name,
177            font_name,
178            font_size,
179            primary_color,
180            secondary_color,
181            outline_color,
182            back_color,
183            formatting,
184            scale_x,
185            scale_y,
186            spacing,
187            angle,
188            border_style,
189            outline,
190            shadow,
191            alignment,
192            margin_l,
193            margin_r,
194            margin_t,
195            margin_b,
196            encoding,
197            complexity_score: 0, // Will be computed
198        };
199
200        Ok(Self {
201            complexity_score: Self::calculate_complexity(&resolved),
202            ..resolved
203        })
204    }
205
206    /// Get font family name
207    #[must_use]
208    pub fn font_name(&self) -> &str {
209        &self.font_name
210    }
211
212    /// Get font size in points
213    #[must_use]
214    pub const fn font_size(&self) -> f32 {
215        self.font_size
216    }
217
218    /// Get primary color as RGBA bytes
219    #[must_use]
220    pub const fn primary_color(&self) -> [u8; 4] {
221        self.primary_color
222    }
223
224    /// Get rendering complexity score (0-100)
225    #[must_use]
226    pub const fn complexity_score(&self) -> u8 {
227        self.complexity_score
228    }
229
230    /// Check if style has performance concerns
231    #[must_use]
232    pub const fn has_performance_issues(&self) -> bool {
233        self.complexity_score > 70
234    }
235
236    /// Get text formatting flags
237    #[must_use]
238    pub const fn formatting(&self) -> TextFormatting {
239        self.formatting
240    }
241
242    /// Check if text is bold
243    #[must_use]
244    pub const fn is_bold(&self) -> bool {
245        self.formatting.contains(TextFormatting::BOLD)
246    }
247
248    /// Check if text is italic
249    #[must_use]
250    pub const fn is_italic(&self) -> bool {
251        self.formatting.contains(TextFormatting::ITALIC)
252    }
253
254    /// Check if text is underlined
255    #[must_use]
256    pub const fn is_underline(&self) -> bool {
257        self.formatting.contains(TextFormatting::UNDERLINE)
258    }
259
260    /// Check if text has strike-through
261    #[must_use]
262    pub const fn is_strike_out(&self) -> bool {
263        self.formatting.contains(TextFormatting::STRIKE_OUT)
264    }
265
266    /// Get left margin in pixels
267    #[must_use]
268    pub const fn margin_l(&self) -> u16 {
269        self.margin_l
270    }
271
272    /// Get right margin in pixels
273    #[must_use]
274    pub const fn margin_r(&self) -> u16 {
275        self.margin_r
276    }
277
278    /// Get top margin in pixels
279    #[must_use]
280    pub const fn margin_t(&self) -> u16 {
281        self.margin_t
282    }
283
284    /// Get bottom margin in pixels
285    #[must_use]
286    pub const fn margin_b(&self) -> u16 {
287        self.margin_b
288    }
289
290    /// Get outline thickness
291    #[must_use]
292    pub const fn outline(&self) -> f32 {
293        self.outline
294    }
295
296    /// Get shadow distance
297    #[must_use]
298    pub const fn shadow(&self) -> f32 {
299        self.shadow
300    }
301
302    /// Get secondary color as RGBA bytes
303    #[must_use]
304    pub const fn secondary_color(&self) -> [u8; 4] {
305        self.secondary_color
306    }
307
308    /// Get outline color as RGBA bytes
309    #[must_use]
310    pub const fn outline_color(&self) -> [u8; 4] {
311        self.outline_color
312    }
313
314    /// Get character spacing
315    #[must_use]
316    pub const fn spacing(&self) -> f32 {
317        self.spacing
318    }
319
320    /// Get text rotation angle
321    #[must_use]
322    pub const fn angle(&self) -> f32 {
323        self.angle
324    }
325
326    /// Create resolved style with inheritance from parent
327    ///
328    /// # Arguments
329    ///
330    /// * `style` - Style definition with possible overrides
331    /// * `parent` - Parent style to inherit from
332    ///
333    /// # Returns
334    ///
335    /// Resolved style inheriting parent properties with child overrides
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if style parsing fails
340    #[allow(clippy::cognitive_complexity)]
341    pub fn from_style_with_parent(style: &'a Style<'a>, parent: &Self) -> Result<Self> {
342        // Start with parent properties
343        let mut resolved = parent.clone();
344
345        // Update name to child's name
346        resolved.name = style.name;
347
348        // Override properties that are not empty/default in child
349        if !style.fontname.is_empty() {
350            resolved.font_name = style.fontname.to_string();
351        }
352
353        if !style.fontsize.is_empty() && style.fontsize != "0" {
354            resolved.font_size = parse_font_size(style.fontsize)?;
355        }
356
357        if !style.primary_colour.is_empty() {
358            resolved.primary_color = parse_color_with_default(style.primary_colour)?;
359        }
360
361        if !style.secondary_colour.is_empty() {
362            resolved.secondary_color = parse_color_with_default(style.secondary_colour)?;
363        }
364
365        if !style.outline_colour.is_empty() {
366            resolved.outline_color = parse_color_with_default(style.outline_colour)?;
367        }
368
369        if !style.back_colour.is_empty() {
370            resolved.back_color = parse_color_with_default(style.back_colour)?;
371        }
372
373        // For formatting flags, only override if value is non-empty
374        let mut formatting = resolved.formatting;
375        if !style.bold.is_empty() {
376            if style.bold == "0" {
377                formatting &= !TextFormatting::BOLD;
378            } else if style.bold == "1" {
379                formatting |= TextFormatting::BOLD;
380            }
381        }
382        if !style.italic.is_empty() {
383            if style.italic == "0" {
384                formatting &= !TextFormatting::ITALIC;
385            } else if style.italic == "1" {
386                formatting |= TextFormatting::ITALIC;
387            }
388        }
389        if !style.underline.is_empty() {
390            if style.underline == "0" {
391                formatting &= !TextFormatting::UNDERLINE;
392            } else if style.underline == "1" {
393                formatting |= TextFormatting::UNDERLINE;
394            }
395        }
396        if !style.strikeout.is_empty() {
397            if style.strikeout == "0" {
398                formatting &= !TextFormatting::STRIKE_OUT;
399            } else if style.strikeout == "1" {
400                formatting |= TextFormatting::STRIKE_OUT;
401            }
402        }
403        resolved.formatting = formatting;
404
405        if !style.scale_x.is_empty() && style.scale_x != "100" {
406            resolved.scale_x = parse_percentage(style.scale_x)?;
407        }
408
409        if !style.scale_y.is_empty() && style.scale_y != "100" {
410            resolved.scale_y = parse_percentage(style.scale_y)?;
411        }
412
413        if !style.spacing.is_empty() && style.spacing != "0" {
414            resolved.spacing = parse_float(style.spacing)?;
415        }
416
417        if !style.angle.is_empty() && style.angle != "0" {
418            resolved.angle = parse_float(style.angle)?;
419        }
420
421        if !style.border_style.is_empty() {
422            resolved.border_style = parse_u8(style.border_style)?;
423        }
424
425        if !style.outline.is_empty() && style.outline != "0" {
426            resolved.outline = parse_float(style.outline)?;
427        }
428
429        if !style.shadow.is_empty() && style.shadow != "0" {
430            resolved.shadow = parse_float(style.shadow)?;
431        }
432
433        if !style.alignment.is_empty() {
434            resolved.alignment = parse_u8(style.alignment)?;
435        }
436
437        if !style.margin_l.is_empty() {
438            resolved.margin_l = parse_u16(style.margin_l)?;
439        }
440
441        if !style.margin_r.is_empty() {
442            resolved.margin_r = parse_u16(style.margin_r)?;
443        }
444
445        // Handle margin inheritance
446        if let (Some(t), Some(b)) = (style.margin_t, style.margin_b) {
447            if !t.is_empty() {
448                resolved.margin_t = parse_u16(t)?;
449            }
450            if !b.is_empty() {
451                resolved.margin_b = parse_u16(b)?;
452            }
453        } else if !style.margin_v.is_empty() && style.margin_v != "0" {
454            let margin_v = parse_u16(style.margin_v)?;
455            resolved.margin_t = margin_v;
456            resolved.margin_b = margin_v;
457        }
458        // If margin_v is empty or "0", keep inherited margins
459
460        if !style.encoding.is_empty() {
461            resolved.encoding = parse_u8(style.encoding)?;
462        }
463
464        // Recalculate complexity score
465        resolved.complexity_score = Self::calculate_complexity(&resolved);
466
467        Ok(resolved)
468    }
469
470    /// Apply resolution scaling to coordinate-based properties
471    ///
472    /// Scales font size, spacing, outline, shadow, and margins based on the
473    /// resolution difference between layout and play resolutions.
474    ///
475    /// # Arguments
476    ///
477    /// * `scale_x` - Horizontal scaling factor (`PlayResX` / `LayoutResX`)
478    /// * `scale_y` - Vertical scaling factor (`PlayResY` / `LayoutResY`)
479    pub fn apply_resolution_scaling(&mut self, scale_x: f32, scale_y: f32) {
480        // Scale font size (use average of X/Y scaling to maintain aspect ratio)
481        let avg_scale = (scale_x + scale_y) / 2.0;
482        self.font_size *= avg_scale;
483
484        // Scale spacing (horizontal)
485        self.spacing *= scale_x;
486
487        // Scale outline and shadow (use average scaling)
488        self.outline *= avg_scale;
489        self.shadow *= avg_scale;
490
491        // Scale margins
492        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
493        {
494            self.margin_l = (f32::from(self.margin_l) * scale_x) as u16;
495            self.margin_r = (f32::from(self.margin_r) * scale_x) as u16;
496            self.margin_t = (f32::from(self.margin_t) * scale_y) as u16;
497            self.margin_b = (f32::from(self.margin_b) * scale_y) as u16;
498        }
499
500        // Recalculate complexity score after scaling
501        self.complexity_score = Self::calculate_complexity(self);
502    }
503
504    /// Calculate rendering complexity score
505    fn calculate_complexity(style: &Self) -> u8 {
506        const EPSILON: f32 = 0.001;
507        let mut score = 0u8;
508
509        if style.font_size > 72.0 {
510            score += 20;
511        } else if style.font_size > 48.0 {
512            score += 10;
513        }
514
515        if style.outline > 4.0 {
516            score += 15;
517        } else if style.outline > 2.0 {
518            score += 8;
519        }
520
521        if style.shadow > 3.0 {
522            score += 10;
523        } else if style.shadow > 1.0 {
524            score += 5;
525        }
526
527        if (style.scale_x - 100.0).abs() > EPSILON || (style.scale_y - 100.0).abs() > EPSILON {
528            score += 10;
529        }
530
531        if style.angle.abs() > EPSILON {
532            score += 15;
533        }
534
535        if style.formatting.contains(TextFormatting::BOLD) {
536            score += 2;
537        }
538        if style.formatting.contains(TextFormatting::ITALIC) {
539            score += 2;
540        }
541        if style
542            .formatting
543            .intersects(TextFormatting::UNDERLINE | TextFormatting::STRIKE_OUT)
544        {
545            score += 5;
546        }
547
548        score.min(100)
549    }
550}
551
552/// Parse font size with validation
553fn parse_font_size(size_str: &str) -> Result<f32> {
554    let size = parse_float(size_str)?;
555    if size <= 0.0 || size > 1000.0 {
556        Err(CoreError::parse("Invalid font size"))
557    } else {
558        Ok(size)
559    }
560}
561
562/// Parse color value with default handling for empty strings
563fn parse_color_with_default(color_str: &str) -> Result<[u8; 4]> {
564    if color_str.trim().is_empty() {
565        Ok([255, 255, 255, 255]) // Default white with full alpha
566    } else {
567        crate::utils::parse_bgr_color(color_str)
568    }
569}
570
571/// Parse boolean flag (0 or 1)
572fn parse_bool_flag(flag_str: &str) -> Result<bool> {
573    match flag_str {
574        "0" => Ok(false),
575        "1" => Ok(true),
576        _ => Err(CoreError::parse("Invalid boolean flag")),
577    }
578}
579
580/// Parse percentage value
581fn parse_percentage(percent_str: &str) -> Result<f32> {
582    let value = parse_float(percent_str)?;
583    if (0.0..=1000.0).contains(&value) {
584        Ok(value)
585    } else {
586        Err(CoreError::parse("Invalid percentage"))
587    }
588}
589
590/// Parse float value with validation
591fn parse_float(float_str: &str) -> Result<f32> {
592    float_str
593        .parse::<f32>()
594        .map_err(|_| CoreError::parse("Invalid float value"))
595}
596
597/// Parse u8 value with validation
598fn parse_u8(u8_str: &str) -> Result<u8> {
599    u8_str
600        .parse::<u8>()
601        .map_err(|_| CoreError::parse("Invalid u8 value"))
602}
603
604/// Parse u16 value with validation
605fn parse_u16(u16_str: &str) -> Result<u16> {
606    u16_str
607        .parse::<u16>()
608        .map_err(|_| CoreError::parse("Invalid u16 value"))
609}
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614    use crate::parser::ast::Span;
615    #[cfg(not(feature = "std"))]
616    fn create_test_style() -> Style<'static> {
617        Style {
618            name: "Test",
619            parent: None,
620            fontname: "Arial",
621            fontsize: "20",
622            primary_colour: "&H00FFFFFF",
623            secondary_colour: "&H000000FF",
624            outline_colour: "&H00000000",
625            back_colour: "&H00000000",
626            bold: "0",
627            italic: "0",
628            underline: "0",
629            strikeout: "0",
630            scale_x: "100",
631            scale_y: "100",
632            spacing: "0",
633            angle: "0",
634            border_style: "1",
635            outline: "2",
636            shadow: "0",
637            alignment: "2",
638            margin_l: "10",
639            margin_r: "10",
640            margin_v: "10",
641            margin_t: None,
642            margin_b: None,
643            encoding: "1",
644            relative_to: None,
645            span: Span::new(0, 0, 0, 0),
646        }
647    }
648
649    #[cfg(feature = "std")]
650    fn create_test_style() -> Style<'static> {
651        Style {
652            name: "Test",
653            parent: None,
654            fontname: "Arial",
655            fontsize: "20",
656            primary_colour: "&H00FFFFFF",
657            secondary_colour: "&H000000FF",
658            outline_colour: "&H00000000",
659            back_colour: "&H00000000",
660            bold: "0",
661            italic: "0",
662            underline: "0",
663            strikeout: "0",
664            scale_x: "100",
665            scale_y: "100",
666            spacing: "0",
667            angle: "0",
668            border_style: "1",
669            outline: "2",
670            shadow: "0",
671            alignment: "2",
672            margin_l: "10",
673            margin_r: "10",
674            margin_v: "10",
675            margin_t: None,
676            margin_b: None,
677            encoding: "1",
678            relative_to: None,
679            span: Span::new(0, 0, 0, 0),
680        }
681    }
682
683    #[test]
684    fn resolved_style_creation() {
685        let style = create_test_style();
686        let resolved = ResolvedStyle::from_style(&style).unwrap();
687
688        assert_eq!(resolved.name, "Test");
689        assert_eq!(resolved.font_name(), "Arial");
690        assert!((resolved.font_size() - 20.0).abs() < f32::EPSILON);
691        assert_eq!(resolved.primary_color(), [255, 255, 255, 0]);
692    }
693
694    #[test]
695    fn color_parsing() {
696        // ASS colors are in BGR format: &HAABBGGRR where AA=alpha, BB=blue, GG=green, RR=red
697        assert_eq!(
698            crate::utils::parse_bgr_color("&H000000FF").unwrap(),
699            [255, 0, 0, 0]
700        ); // Red: RR=FF
701        assert_eq!(
702            crate::utils::parse_bgr_color("&H0000FF00").unwrap(),
703            [0, 255, 0, 0]
704        ); // Green: GG=FF
705        assert_eq!(
706            crate::utils::parse_bgr_color("&H00FF0000").unwrap(),
707            [0, 0, 255, 0]
708        ); // Blue: BB=FF
709
710        // Test case-insensitive prefix
711        assert_eq!(
712            crate::utils::parse_bgr_color("&h000000FF").unwrap(),
713            [255, 0, 0, 0]
714        ); // Red with lowercase h
715
716        // Test 6-digit format (no alpha channel)
717        assert_eq!(
718            crate::utils::parse_bgr_color("&HFF0000").unwrap(),
719            [0, 0, 255, 0]
720        ); // Blue in 6-digit
721        assert_eq!(
722            crate::utils::parse_bgr_color("&H00FF00").unwrap(),
723            [0, 255, 0, 0]
724        ); // Green in 6-digit
725        assert_eq!(
726            crate::utils::parse_bgr_color("&H0000FF").unwrap(),
727            [255, 0, 0, 0]
728        ); // Red in 6-digit
729    }
730
731    #[test]
732    fn complexity_scoring() {
733        let mut style = create_test_style();
734
735        let resolved = ResolvedStyle::from_style(&style).unwrap();
736        assert!(resolved.complexity_score() < 50);
737
738        style.fontsize = "100";
739        let resolved = ResolvedStyle::from_style(&style).unwrap();
740        assert!(resolved.complexity_score() >= 20);
741    }
742
743    #[test]
744    fn performance_issues_detection() {
745        let mut style = create_test_style();
746
747        let resolved = ResolvedStyle::from_style(&style).unwrap();
748        assert!(!resolved.has_performance_issues());
749
750        // Create a style with multiple performance-affecting properties
751        style.fontsize = "120"; // >72: +20 points
752        style.outline = "8"; // >4: +15 points
753        style.shadow = "5"; // >3: +10 points
754        style.angle = "45"; // !=0: +15 points
755        style.scale_x = "150"; // !=100: +10 points
756        style.bold = "1"; // +2 points
757        style.italic = "1"; // +2 points
758        style.underline = "1"; // +5 points
759                               // Total: 79 points > 70 threshold
760
761        let resolved = ResolvedStyle::from_style(&style).unwrap();
762        assert!(resolved.has_performance_issues());
763    }
764
765    #[test]
766    fn parse_font_size_edge_cases() {
767        // Test invalid font sizes
768        assert!(parse_font_size("-10").is_err()); // Negative
769        assert!(parse_font_size("0").is_err()); // Zero
770        assert!(parse_font_size("1001").is_err()); // Too large
771        assert!(parse_font_size("abc").is_err()); // Non-numeric
772        assert!(parse_font_size("").is_err()); // Empty
773
774        // Test valid font sizes
775        assert!(parse_font_size("1").is_ok());
776        assert!(parse_font_size("72").is_ok());
777        assert!(parse_font_size("1000").is_ok());
778    }
779
780    #[test]
781    fn parse_color_with_default_invalid_formats() {
782        // Test invalid color formats
783        assert!(parse_color_with_default("invalid").is_err());
784        assert!(parse_color_with_default("&H").is_err());
785        assert!(parse_color_with_default("&HZZZZZ").is_err());
786        assert!(parse_color_with_default("12345G").is_err()); // Invalid hex character
787
788        // Test empty string returns default
789        let default_color = parse_color_with_default("").unwrap();
790        assert_eq!(default_color, [255, 255, 255, 255]);
791
792        // Test whitespace only returns default
793        let whitespace_color = parse_color_with_default("   ").unwrap();
794        assert_eq!(whitespace_color, [255, 255, 255, 255]);
795    }
796
797    #[test]
798    fn parse_bool_flag_invalid_values() {
799        // Test invalid boolean flags
800        assert!(parse_bool_flag("2").is_err());
801        assert!(parse_bool_flag("-1").is_err());
802        assert!(parse_bool_flag("true").is_err());
803        assert!(parse_bool_flag("false").is_err());
804        assert!(parse_bool_flag("yes").is_err());
805        assert!(parse_bool_flag("no").is_err());
806        assert!(parse_bool_flag("").is_err());
807
808        // Test valid boolean flags
809        assert!(!parse_bool_flag("0").unwrap());
810        assert!(parse_bool_flag("1").unwrap());
811    }
812
813    #[test]
814    #[allow(clippy::float_cmp)]
815    fn parse_percentage_invalid_values() {
816        // Test invalid percentages
817        assert!(parse_percentage("-10").is_err()); // Negative
818        assert!(parse_percentage("1001").is_err()); // Too large
819        assert!(parse_percentage("abc").is_err()); // Non-numeric
820        assert!(parse_percentage("").is_err()); // Empty
821
822        // Test valid percentages
823        assert_eq!(parse_percentage("0").unwrap(), 0.0);
824        assert_eq!(parse_percentage("100").unwrap(), 100.0);
825        assert_eq!(parse_percentage("1000").unwrap(), 1000.0);
826    }
827
828    #[test]
829    #[allow(clippy::float_cmp)]
830    fn parse_float_invalid_values() {
831        assert!(parse_float("abc").is_err());
832        assert!(parse_float("").is_err());
833        assert!(parse_float("1.2.3").is_err());
834        assert!(parse_float("1.2.3.4").is_err());
835        assert!(parse_float("not_a_number").is_err());
836
837        // Test valid floats
838        assert_eq!(parse_float("0").unwrap(), 0.0);
839        assert_eq!(parse_float("-10.5").unwrap(), -10.5);
840        assert_eq!(parse_float("123.456").unwrap(), 123.456);
841    }
842
843    #[test]
844    fn parse_u8_invalid_values() {
845        assert!(parse_u8("256").is_err()); // Too large
846        assert!(parse_u8("-1").is_err()); // Negative
847        assert!(parse_u8("abc").is_err()); // Non-numeric
848        assert!(parse_u8("").is_err()); // Empty
849
850        // Test valid u8 values
851        assert_eq!(parse_u8("0").unwrap(), 0);
852        assert_eq!(parse_u8("255").unwrap(), 255);
853    }
854
855    #[test]
856    fn parse_u16_invalid_values() {
857        assert!(parse_u16("65536").is_err()); // Too large
858        assert!(parse_u16("-1").is_err()); // Negative
859        assert!(parse_u16("abc").is_err()); // Non-numeric
860        assert!(parse_u16("").is_err()); // Empty
861
862        // Test valid u16 values
863        assert_eq!(parse_u16("0").unwrap(), 0);
864        assert_eq!(parse_u16("65535").unwrap(), 65535);
865    }
866
867    #[test]
868    fn resolved_style_from_style_with_invalid_values() {
869        let mut style = create_test_style();
870
871        // Test with invalid font size - should return error
872        style.fontsize = "-10";
873        assert!(ResolvedStyle::from_style(&style).is_err());
874
875        style.fontsize = "abc";
876        assert!(ResolvedStyle::from_style(&style).is_err());
877
878        // Test with invalid color - should return error
879        style.fontsize = "20"; // Reset to valid
880        style.primary_colour = "invalid_color";
881        assert!(ResolvedStyle::from_style(&style).is_err());
882
883        // Test with invalid boolean flag - should return error
884        style.primary_colour = "&HFFFFFF"; // Reset to valid
885        style.bold = "2";
886        assert!(ResolvedStyle::from_style(&style).is_err());
887    }
888
889    #[test]
890    fn complexity_calculation_all_branches() {
891        let mut style = create_test_style();
892
893        // Test baseline complexity
894        let resolved = ResolvedStyle::from_style(&style).unwrap();
895        let baseline_score = resolved.complexity_score();
896
897        // Test font size increases complexity
898        style.fontsize = "100"; // Large font size
899        let resolved = ResolvedStyle::from_style(&style).unwrap();
900        assert!(resolved.complexity_score() > baseline_score);
901
902        // Test outline increases complexity
903        style = create_test_style(); // Reset
904        style.outline = "5"; // Large outline
905        let resolved = ResolvedStyle::from_style(&style).unwrap();
906        assert!(resolved.complexity_score() > baseline_score);
907
908        // Test shadow increases complexity
909        style = create_test_style(); // Reset
910        style.shadow = "5"; // Large shadow
911        let resolved = ResolvedStyle::from_style(&style).unwrap();
912        assert!(resolved.complexity_score() > baseline_score);
913
914        // Test scaling increases complexity
915        style = create_test_style(); // Reset
916        style.scale_x = "200"; // Non-default scaling
917        let resolved = ResolvedStyle::from_style(&style).unwrap();
918        assert!(resolved.complexity_score() > baseline_score);
919
920        // Test angle increases complexity
921        style = create_test_style(); // Reset
922        style.angle = "45"; // Rotation
923        let resolved = ResolvedStyle::from_style(&style).unwrap();
924        assert!(resolved.complexity_score() > baseline_score);
925
926        // Test formatting flags increase complexity
927        style = create_test_style(); // Reset
928        style.bold = "1";
929        style.italic = "1";
930        style.underline = "1";
931        let resolved = ResolvedStyle::from_style(&style).unwrap();
932        assert!(resolved.complexity_score() > baseline_score);
933    }
934
935    #[test]
936    fn complexity_score_capped_at_100() {
937        let mut style = create_test_style();
938
939        // Set all properties to maximum complexity values
940        style.fontsize = "200"; // Large font
941        style.outline = "10"; // Large outline
942        style.shadow = "10"; // Large shadow
943        style.scale_x = "200"; // Large scaling
944        style.angle = "180"; // Large rotation
945        style.bold = "1";
946        style.italic = "1";
947        style.underline = "1";
948        style.strikeout = "1";
949
950        let resolved = ResolvedStyle::from_style(&style).unwrap();
951        assert!(resolved.complexity_score() <= 100); // Should be capped at 100
952        assert!(resolved.complexity_score() > 50); // Should be high complexity
953    }
954
955    #[test]
956    fn text_formatting_flags_comprehensive() {
957        let mut style = create_test_style();
958
959        // Test all formatting combinations
960        style.bold = "1";
961        style.italic = "0";
962        style.underline = "0";
963        style.strikeout = "0";
964        let resolved = ResolvedStyle::from_style(&style).unwrap();
965        assert!(resolved.is_bold());
966        assert!(!resolved.is_italic());
967        assert!(!resolved.is_underline());
968        assert!(!resolved.is_strike_out());
969        assert_eq!(resolved.formatting(), TextFormatting::BOLD);
970
971        // Test italic only
972        style.bold = "0";
973        style.italic = "1";
974        let resolved = ResolvedStyle::from_style(&style).unwrap();
975        assert!(!resolved.is_bold());
976        assert!(resolved.is_italic());
977        assert_eq!(resolved.formatting(), TextFormatting::ITALIC);
978
979        // Test underline only
980        style.italic = "0";
981        style.underline = "1";
982        let resolved = ResolvedStyle::from_style(&style).unwrap();
983        assert!(resolved.is_underline());
984        assert_eq!(resolved.formatting(), TextFormatting::UNDERLINE);
985
986        // Test strikeout only
987        style.underline = "0";
988        style.strikeout = "1";
989        let resolved = ResolvedStyle::from_style(&style).unwrap();
990        assert!(resolved.is_strike_out());
991        assert_eq!(resolved.formatting(), TextFormatting::STRIKE_OUT);
992
993        // Test all flags combined
994        style.bold = "1";
995        style.italic = "1";
996        style.underline = "1";
997        style.strikeout = "1";
998        let resolved = ResolvedStyle::from_style(&style).unwrap();
999        assert!(resolved.is_bold());
1000        assert!(resolved.is_italic());
1001        assert!(resolved.is_underline());
1002        assert!(resolved.is_strike_out());
1003        let expected = TextFormatting::BOLD
1004            | TextFormatting::ITALIC
1005            | TextFormatting::UNDERLINE
1006            | TextFormatting::STRIKE_OUT;
1007        assert_eq!(resolved.formatting(), expected);
1008    }
1009
1010    #[test]
1011    fn resolved_style_empty_font_name_uses_default() {
1012        let mut style = create_test_style();
1013        style.fontname = "";
1014
1015        let resolved = ResolvedStyle::from_style(&style).unwrap();
1016        assert_eq!(resolved.font_name(), "Arial");
1017    }
1018
1019    #[test]
1020    #[allow(clippy::float_cmp)]
1021    fn resolved_style_getters_comprehensive() {
1022        let style = create_test_style();
1023        let resolved = ResolvedStyle::from_style(&style).unwrap();
1024
1025        // Test all getter methods
1026        assert_eq!(resolved.font_name(), "Arial");
1027        assert_eq!(resolved.font_size(), 20.0);
1028        assert_eq!(resolved.primary_color(), [255, 255, 255, 0]); // &H00FFFFFF
1029        assert!(!resolved.has_performance_issues()); // Low complexity
1030
1031        let formatting = resolved.formatting();
1032        assert!(!resolved.is_bold());
1033        assert!(!resolved.is_italic());
1034        assert!(!resolved.is_underline());
1035        assert!(!resolved.is_strike_out());
1036        assert_eq!(formatting, TextFormatting::empty());
1037    }
1038
1039    #[test]
1040    fn resolved_style_apply_resolution_scaling_symmetric() {
1041        let style = create_test_style();
1042        let mut resolved = ResolvedStyle::from_style(&style).unwrap();
1043
1044        // Apply 2x scaling
1045        resolved.apply_resolution_scaling(2.0, 2.0);
1046
1047        assert!((resolved.font_size() - 40.0).abs() < f32::EPSILON); // 20 * 2
1048        assert!((resolved.spacing() - 0.0).abs() < f32::EPSILON); // 0 * 2
1049        assert!((resolved.outline() - 4.0).abs() < f32::EPSILON); // 2 * 2
1050        assert!((resolved.shadow() - 0.0).abs() < f32::EPSILON); // 0 * 2
1051        assert_eq!(resolved.margin_l(), 20); // 10 * 2
1052        assert_eq!(resolved.margin_r(), 20); // 10 * 2
1053        assert_eq!(resolved.margin_t(), 20); // 10 * 2
1054        assert_eq!(resolved.margin_b(), 20); // 10 * 2
1055    }
1056
1057    #[test]
1058    fn resolved_style_apply_resolution_scaling_asymmetric() {
1059        let mut style = create_test_style();
1060        style.spacing = "4";
1061        style.shadow = "2";
1062        style.margin_l = "10";
1063        style.margin_r = "20";
1064        style.margin_v = "30";
1065
1066        let mut resolved = ResolvedStyle::from_style(&style).unwrap();
1067
1068        // Apply asymmetric scaling (3x horizontal, 2x vertical)
1069        resolved.apply_resolution_scaling(3.0, 2.0);
1070
1071        // Average scale for font/outline/shadow: (3 + 2) / 2 = 2.5
1072        assert!((resolved.font_size() - 50.0).abs() < f32::EPSILON); // 20 * 2.5
1073        assert!((resolved.spacing() - 12.0).abs() < f32::EPSILON); // 4 * 3
1074        assert!((resolved.outline() - 5.0).abs() < f32::EPSILON); // 2 * 2.5
1075        assert!((resolved.shadow() - 5.0).abs() < f32::EPSILON); // 2 * 2.5
1076        assert_eq!(resolved.margin_l(), 30); // 10 * 3
1077        assert_eq!(resolved.margin_r(), 60); // 20 * 3
1078        assert_eq!(resolved.margin_t(), 60); // 30 * 2
1079        assert_eq!(resolved.margin_b(), 60); // 30 * 2
1080    }
1081
1082    #[test]
1083    fn resolved_style_apply_resolution_scaling_downscale() {
1084        let style = create_test_style();
1085        let mut resolved = ResolvedStyle::from_style(&style).unwrap();
1086
1087        // Apply 0.5x scaling (downscale)
1088        resolved.apply_resolution_scaling(0.5, 0.5);
1089
1090        assert!((resolved.font_size() - 10.0).abs() < f32::EPSILON); // 20 * 0.5
1091        assert!((resolved.spacing() - 0.0).abs() < f32::EPSILON); // 0 * 0.5
1092        assert!((resolved.outline() - 1.0).abs() < f32::EPSILON); // 2 * 0.5
1093        assert!((resolved.shadow() - 0.0).abs() < f32::EPSILON); // 0 * 0.5
1094        assert_eq!(resolved.margin_l(), 5); // 10 * 0.5
1095        assert_eq!(resolved.margin_r(), 5); // 10 * 0.5
1096        assert_eq!(resolved.margin_t(), 5); // 10 * 0.5
1097        assert_eq!(resolved.margin_b(), 5); // 10 * 0.5
1098    }
1099
1100    #[test]
1101    fn resolved_style_apply_resolution_scaling_updates_complexity() {
1102        let mut style = create_test_style();
1103        style.fontsize = "30"; // Not quite large enough to trigger complexity
1104
1105        let mut resolved = ResolvedStyle::from_style(&style).unwrap();
1106        let initial_complexity = resolved.complexity_score();
1107
1108        // Apply 3x scaling to push font size over complexity threshold
1109        resolved.apply_resolution_scaling(3.0, 3.0);
1110
1111        assert!((resolved.font_size() - 90.0).abs() < f32::EPSILON); // 30 * 3
1112        assert!(resolved.complexity_score() > initial_complexity); // Should increase due to large font
1113    }
1114
1115    #[test]
1116    fn resolved_style_apply_resolution_scaling_preserves_other_properties() {
1117        let mut style = create_test_style();
1118        style.bold = "1";
1119        style.italic = "1";
1120        style.primary_colour = "&H00FF0000"; // Red
1121        style.angle = "45";
1122
1123        let mut resolved = ResolvedStyle::from_style(&style).unwrap();
1124        let initial_color = resolved.primary_color();
1125        let initial_angle = resolved.angle;
1126        let initial_formatting = resolved.formatting();
1127
1128        // Apply scaling
1129        resolved.apply_resolution_scaling(2.0, 2.0);
1130
1131        // These properties should not be affected by scaling
1132        assert_eq!(resolved.primary_color(), initial_color);
1133        assert!((resolved.angle - initial_angle).abs() < f32::EPSILON);
1134        assert_eq!(resolved.formatting(), initial_formatting);
1135        assert!(resolved.is_bold());
1136        assert!(resolved.is_italic());
1137    }
1138
1139    #[test]
1140    fn resolved_style_spacing_getter() {
1141        let mut style = create_test_style();
1142        style.spacing = "5.5";
1143
1144        let resolved = ResolvedStyle::from_style(&style).unwrap();
1145        assert!((resolved.spacing() - 5.5).abs() < f32::EPSILON);
1146    }
1147
1148    #[test]
1149    fn resolved_style_angle_getter() {
1150        let mut style = create_test_style();
1151        style.angle = "45.5";
1152
1153        let resolved = ResolvedStyle::from_style(&style).unwrap();
1154        assert!((resolved.angle() - 45.5).abs() < f32::EPSILON);
1155    }
1156}