Skip to main content

fop_core/properties/
validation.rs

1//! Property validation with range checks and allowed enum values
2//!
3//! This module implements comprehensive validation rules for XSL-FO properties
4//! based on Section 5 of the XSL-FO 1.1 specification.
5//!
6//! # Features
7//! - Range validation for numeric and length properties
8//! - Enum validation with allowed value sets
9//! - Required property checks per element type
10//! - Mutual exclusion constraints
11//! - Detailed error messages with suggestions
12
13use crate::properties::{PropertyId, PropertyValue};
14use fop_types::{FopError, Result};
15use std::collections::HashMap;
16
17/// Validation error with detailed message and suggestion
18#[derive(Debug, Clone, PartialEq)]
19pub struct ValidationError {
20    /// Property that failed validation
21    pub property: PropertyId,
22    /// Error message describing what went wrong
23    pub message: String,
24    /// Optional suggestion for how to fix it
25    pub suggestion: Option<String>,
26}
27
28impl ValidationError {
29    /// Create a new validation error
30    pub fn new(property: PropertyId, message: String, suggestion: Option<String>) -> Self {
31        Self {
32            property,
33            message,
34            suggestion,
35        }
36    }
37
38    /// Convert to FopError
39    pub fn to_fop_error(&self, value: &PropertyValue) -> FopError {
40        let mut reason = self.message.clone();
41        if let Some(ref suggestion) = self.suggestion {
42            reason.push_str(" Suggestion: ");
43            reason.push_str(suggestion);
44        }
45        FopError::PropertyValidation {
46            property: self.property.name().to_string(),
47            value: format!("{:?}", value),
48            reason,
49        }
50    }
51}
52
53/// Validation rule for a property
54#[derive(Debug, Clone, PartialEq)]
55pub enum ValidationRule {
56    /// Range validation for numbers (min, max)
57    Range { min: f64, max: f64 },
58    /// Enum validation (allowed values as u16)
59    Enum { allowed: Vec<u16> },
60    /// String enum validation (allowed string values)
61    StringEnum { allowed: Vec<&'static str> },
62    /// Required property (must be present)
63    Required,
64    /// Conditional validation (when property X has value Y, this must be Z)
65    Conditional {
66        when: PropertyId,
67        has: PropertyValue,
68    },
69}
70
71/// Property validator with rule-based validation
72pub struct PropertyValidator {
73    rules: HashMap<PropertyId, Vec<ValidationRule>>,
74}
75
76impl Default for PropertyValidator {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl PropertyValidator {
83    /// Create a new property validator with default rules
84    pub fn new() -> Self {
85        let mut validator = Self {
86            rules: HashMap::new(),
87        };
88        validator.register_default_rules();
89        validator
90    }
91
92    /// Register default validation rules
93    fn register_default_rules(&mut self) {
94        // Opacity: 0.0 - 1.0
95        self.add_rule(
96            PropertyId::Opacity,
97            ValidationRule::Range { min: 0.0, max: 1.0 },
98        );
99
100        // Column-count: 1 - 255
101        self.add_rule(
102            PropertyId::ColumnCount,
103            ValidationRule::Range {
104                min: 1.0,
105                max: 255.0,
106            },
107        );
108
109        // Orphans: 1 - 999
110        self.add_rule(
111            PropertyId::Orphans,
112            ValidationRule::Range {
113                min: 1.0,
114                max: 999.0,
115            },
116        );
117
118        // Widows: 1 - 999
119        self.add_rule(
120            PropertyId::Widows,
121            ValidationRule::Range {
122                min: 1.0,
123                max: 999.0,
124            },
125        );
126
127        // Z-index: -999999 - 999999
128        self.add_rule(
129            PropertyId::ZIndex,
130            ValidationRule::Range {
131                min: -999999.0,
132                max: 999999.0,
133            },
134        );
135
136        // Text-align enum values
137        self.add_rule(
138            PropertyId::TextAlign,
139            ValidationRule::StringEnum {
140                allowed: vec!["start", "end", "center", "justify", "left", "right"],
141            },
142        );
143
144        // Overflow enum values
145        self.add_rule(
146            PropertyId::Overflow,
147            ValidationRule::StringEnum {
148                allowed: vec!["visible", "hidden", "scroll", "auto"],
149            },
150        );
151
152        // Border styles (applied to all border-style properties)
153        let border_style_enum = ValidationRule::StringEnum {
154            allowed: vec![
155                "none", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset",
156            ],
157        };
158        self.add_rule(PropertyId::BorderStyle, border_style_enum.clone());
159        self.add_rule(PropertyId::BorderTopStyle, border_style_enum.clone());
160        self.add_rule(PropertyId::BorderRightStyle, border_style_enum.clone());
161        self.add_rule(PropertyId::BorderBottomStyle, border_style_enum.clone());
162        self.add_rule(PropertyId::BorderLeftStyle, border_style_enum.clone());
163        self.add_rule(PropertyId::BorderBeforeStyle, border_style_enum.clone());
164        self.add_rule(PropertyId::BorderAfterStyle, border_style_enum.clone());
165        self.add_rule(PropertyId::BorderStartStyle, border_style_enum.clone());
166        self.add_rule(PropertyId::BorderEndStyle, border_style_enum);
167
168        // Break-before and break-after enum values
169        let break_enum = ValidationRule::StringEnum {
170            allowed: vec!["auto", "column", "page", "even-page", "odd-page"],
171        };
172        self.add_rule(PropertyId::BreakBefore, break_enum.clone());
173        self.add_rule(PropertyId::BreakAfter, break_enum);
174    }
175
176    /// Add a validation rule for a property
177    pub fn add_rule(&mut self, property: PropertyId, rule: ValidationRule) {
178        self.rules.entry(property).or_default().push(rule);
179    }
180
181    /// Validate a property value according to registered rules (instance method)
182    pub fn validate(&self, id: PropertyId, value: &PropertyValue) -> Result<()> {
183        // Auto, Inherit, and None are generally always valid
184        if matches!(
185            value,
186            PropertyValue::Auto | PropertyValue::Inherit | PropertyValue::None
187        ) {
188            return Ok(());
189        }
190
191        // Check registered rules
192        if let Some(rules) = self.rules.get(&id) {
193            for rule in rules {
194                self.validate_rule(id, value, rule)?;
195            }
196        }
197
198        // Additional hardcoded validations
199        match id {
200            PropertyId::FontSize => Self::validate_positive_length(id, value),
201            PropertyId::LineHeight => Self::validate_line_height(value),
202            _ => Ok(()),
203        }
204    }
205
206    /// Validate a property value (static method for backward compatibility)
207    ///
208    /// This creates a default validator and validates the value.
209    /// For better performance with multiple validations, create a PropertyValidator
210    /// instance and reuse it.
211    pub fn validate_static(id: PropertyId, value: &PropertyValue) -> Result<()> {
212        // Auto, Inherit, and None are generally always valid
213        if matches!(
214            value,
215            PropertyValue::Auto | PropertyValue::Inherit | PropertyValue::None
216        ) {
217            return Ok(());
218        }
219
220        // For static validation, we only do basic hardcoded checks
221        // to avoid creating a validator instance every time
222        match id {
223            PropertyId::FontSize => Self::validate_positive_length(id, value),
224            PropertyId::ColumnCount => Self::validate_positive_integer(id, value, 1, 255),
225            PropertyId::Widows => Self::validate_positive_integer(id, value, 1, 999),
226            PropertyId::Orphans => Self::validate_positive_integer(id, value, 1, 999),
227            PropertyId::ZIndex => Self::validate_integer_range(id, value, -999999, 999999),
228            PropertyId::LineHeight => Self::validate_line_height(value),
229            PropertyId::Opacity => Self::validate_range(id, value, 0.0, 1.0),
230            _ => Ok(()),
231        }
232    }
233
234    /// Validate a single rule
235    fn validate_rule(
236        &self,
237        id: PropertyId,
238        value: &PropertyValue,
239        rule: &ValidationRule,
240    ) -> Result<()> {
241        match rule {
242            ValidationRule::Range { min, max } => Self::validate_range(id, value, *min, *max),
243            ValidationRule::Enum { allowed } => Self::validate_enum_values(id, value, allowed),
244            ValidationRule::StringEnum { allowed } => {
245                Self::validate_string_enum(id, value, allowed)
246            }
247            ValidationRule::Required => {
248                // Required checks are typically done at element level
249                Ok(())
250            }
251            ValidationRule::Conditional { .. } => {
252                // Conditional checks require context
253                Ok(())
254            }
255        }
256    }
257
258    /// Validate that a value is within the specified range
259    fn validate_range(id: PropertyId, value: &PropertyValue, min: f64, max: f64) -> Result<()> {
260        let num = match value {
261            PropertyValue::Number(n) => *n,
262            PropertyValue::Integer(i) => *i as f64,
263            PropertyValue::Length(len) => len.to_pt(),
264            PropertyValue::Percentage(p) => p.as_fraction(),
265            _ => {
266                return Err(FopError::PropertyValidation {
267                    property: id.name().to_string(),
268                    value: format!("{:?}", value),
269                    reason: "Expected numeric value for range validation".to_string(),
270                });
271            }
272        };
273
274        if num < min || num > max {
275            let error = ValidationError::new(
276                id,
277                format!("Value {} is out of range (must be {}-{})", num, min, max),
278                Some(Self::get_range_suggestion(id, min, max)),
279            );
280            return Err(error.to_fop_error(value));
281        }
282
283        Ok(())
284    }
285
286    /// Get a helpful suggestion for range validation
287    fn get_range_suggestion(id: PropertyId, min: f64, max: f64) -> String {
288        match id {
289            PropertyId::Opacity => {
290                "Use a value between 0.0 (fully transparent) and 1.0 (fully opaque)".to_string()
291            }
292            PropertyId::ColumnCount => {
293                format!("Column count must be between {} and {} columns", min, max)
294            }
295            PropertyId::Orphans => format!(
296                "Orphans must be between {} and {} lines at the bottom of a page",
297                min, max
298            ),
299            PropertyId::Widows => format!(
300                "Widows must be between {} and {} lines at the top of a page",
301                min, max
302            ),
303            PropertyId::ZIndex => {
304                format!("Z-index must be between {} and {}", min, max)
305            }
306            _ => format!("Value must be between {} and {}", min, max),
307        }
308    }
309
310    /// Validate enum values (u16)
311    fn validate_enum_values(id: PropertyId, value: &PropertyValue, allowed: &[u16]) -> Result<()> {
312        match value {
313            PropertyValue::Enum(e) => {
314                if !allowed.contains(e) {
315                    return Err(FopError::PropertyValidation {
316                        property: id.name().to_string(),
317                        value: format!("{}", e),
318                        reason: format!("Allowed enum values: {:?}", allowed),
319                    });
320                }
321                Ok(())
322            }
323            _ => Err(FopError::PropertyValidation {
324                property: id.name().to_string(),
325                value: format!("{:?}", value),
326                reason: "Expected enum value".to_string(),
327            }),
328        }
329    }
330
331    /// Validate string enum values
332    fn validate_string_enum(
333        id: PropertyId,
334        value: &PropertyValue,
335        allowed: &[&'static str],
336    ) -> Result<()> {
337        match value {
338            PropertyValue::String(s) => {
339                if !allowed.contains(&s.as_ref()) {
340                    let error = ValidationError::new(
341                        id,
342                        format!("'{}' is not a valid value", s),
343                        Some(format!("Use one of: {}", allowed.join(", "))),
344                    );
345                    return Err(error.to_fop_error(value));
346                }
347                Ok(())
348            }
349            _ => Err(FopError::PropertyValidation {
350                property: id.name().to_string(),
351                value: format!("{:?}", value),
352                reason: format!("Expected one of: {}", allowed.join(", ")),
353            }),
354        }
355    }
356
357    /// Check for mutual exclusion between properties
358    pub fn check_mutual_exclusion(
359        &self,
360        props: &[(PropertyId, PropertyValue)],
361    ) -> Vec<ValidationError> {
362        let mut errors = Vec::new();
363
364        // Check width vs inline-progression-dimension
365        let has_width = props.iter().any(|(id, _)| *id == PropertyId::Width);
366        let has_ipd = props
367            .iter()
368            .any(|(id, _)| *id == PropertyId::InlineProgressionDimension);
369        if has_width && has_ipd {
370            errors.push(ValidationError::new(
371                PropertyId::Width,
372                "Both 'width' and 'inline-progression-dimension' are set".to_string(),
373                Some(
374                    "Use 'width' for simple cases or 'inline-progression-dimension' for advanced control, but not both"
375                        .to_string(),
376                ),
377            ));
378        }
379
380        // Check height vs block-progression-dimension
381        let has_height = props.iter().any(|(id, _)| *id == PropertyId::Height);
382        let has_bpd = props
383            .iter()
384            .any(|(id, _)| *id == PropertyId::BlockProgressionDimension);
385        if has_height && has_bpd {
386            errors.push(ValidationError::new(
387                PropertyId::Height,
388                "Both 'height' and 'block-progression-dimension' are set".to_string(),
389                Some(
390                    "Use 'height' for simple cases or 'block-progression-dimension' for advanced control, but not both"
391                        .to_string(),
392                ),
393            ));
394        }
395
396        errors
397    }
398
399    /// Validate length range
400    pub fn validate_length_range(
401        id: PropertyId,
402        value: &PropertyValue,
403        min: f64,
404        max: f64,
405    ) -> Result<()> {
406        match value {
407            PropertyValue::Length(len) => {
408                let pts = len.to_pt();
409                if pts < min || pts > max {
410                    let error = ValidationError::new(
411                        id,
412                        format!("Length value {} is out of range ({}-{})", pts, min, max),
413                        Some(format!("Value must be between {}pt and {}pt", min, max)),
414                    );
415                    return Err(error.to_fop_error(value));
416                }
417                Ok(())
418            }
419            PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
420            _ => Err(FopError::PropertyValidation {
421                property: id.name().to_string(),
422                value: format!("{:?}", value),
423                reason: "Expected length value".to_string(),
424            }),
425        }
426    }
427
428    /// Validate number range
429    pub fn validate_number_range(
430        id: PropertyId,
431        value: &PropertyValue,
432        min: f64,
433        max: f64,
434    ) -> Result<()> {
435        match value {
436            PropertyValue::Number(n) => {
437                if *n < min || *n > max {
438                    let error = ValidationError::new(
439                        id,
440                        format!("Number value {} is out of range ({}-{})", n, min, max),
441                        Some(format!("Value must be between {} and {}", min, max)),
442                    );
443                    return Err(error.to_fop_error(value));
444                }
445                Ok(())
446            }
447            PropertyValue::Integer(i) => {
448                let n = *i as f64;
449                if n < min || n > max {
450                    let error = ValidationError::new(
451                        id,
452                        format!("Integer value {} is out of range ({}-{})", i, min, max),
453                        Some(format!("Value must be between {} and {}", min, max)),
454                    );
455                    return Err(error.to_fop_error(value));
456                }
457                Ok(())
458            }
459            PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
460            _ => Err(FopError::PropertyValidation {
461                property: id.name().to_string(),
462                value: format!("{:?}", value),
463                reason: "Expected numeric value".to_string(),
464            }),
465        }
466    }
467
468    /// Validate percentage range
469    pub fn validate_percentage_range(
470        id: PropertyId,
471        value: &PropertyValue,
472        min: f32,
473        max: f32,
474    ) -> Result<()> {
475        match value {
476            PropertyValue::Percentage(p) => {
477                let fraction = p.as_fraction() as f32;
478                if fraction < min || fraction > max {
479                    let error = ValidationError::new(
480                        id,
481                        format!(
482                            "Percentage value {}% is out of range ({}%-{}%)",
483                            fraction * 100.0,
484                            min * 100.0,
485                            max * 100.0
486                        ),
487                        Some(format!(
488                            "Value must be between {}% and {}%",
489                            min * 100.0,
490                            max * 100.0
491                        )),
492                    );
493                    return Err(error.to_fop_error(value));
494                }
495                Ok(())
496            }
497            PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
498            _ => Err(FopError::PropertyValidation {
499                property: id.name().to_string(),
500                value: format!("{:?}", value),
501                reason: "Expected percentage value".to_string(),
502            }),
503        }
504    }
505
506    /// Validate enum (legacy method for backward compatibility)
507    pub fn validate_enum(
508        id: PropertyId,
509        value: &PropertyValue,
510        allowed_values: &[u16],
511    ) -> Result<()> {
512        Self::validate_enum_values(id, value, allowed_values)
513    }
514
515    /// Validate that a length is positive
516    fn validate_positive_length(id: PropertyId, value: &PropertyValue) -> Result<()> {
517        match value {
518            PropertyValue::Length(len) => {
519                if len.to_pt() <= 0.0 {
520                    let error = ValidationError::new(
521                        id,
522                        format!("Length value {} must be positive", len.to_pt()),
523                        Some("Font size must be greater than 0".to_string()),
524                    );
525                    return Err(error.to_fop_error(value));
526                }
527                Ok(())
528            }
529            // Relative/absolute font-size keywords (xx-small, small, medium, large, larger, etc.)
530            PropertyValue::RelativeFontSize(_) => Ok(()),
531            // Percentage values (e.g. 70% or em-based values stored as percentage)
532            PropertyValue::Percentage(_) => Ok(()),
533            // calc() expressions and dynamic values
534            PropertyValue::Expression(_) => Ok(()),
535            PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
536            _ => Err(FopError::PropertyValidation {
537                property: id.name().to_string(),
538                value: format!("{:?}", value),
539                reason: "Expected length value".to_string(),
540            }),
541        }
542    }
543
544    /// Validate that an integer is within range (inclusive)
545    fn validate_positive_integer(
546        id: PropertyId,
547        value: &PropertyValue,
548        min: i32,
549        max: i32,
550    ) -> Result<()> {
551        match value {
552            PropertyValue::Integer(i) => {
553                if *i < min || *i > max {
554                    let error = ValidationError::new(
555                        id,
556                        format!("Integer value {} is out of range ({}-{})", i, min, max),
557                        Some(Self::get_range_suggestion(id, min as f64, max as f64)),
558                    );
559                    return Err(error.to_fop_error(value));
560                }
561                Ok(())
562            }
563            PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
564            _ => Err(FopError::PropertyValidation {
565                property: id.name().to_string(),
566                value: format!("{:?}", value),
567                reason: "Expected integer value".to_string(),
568            }),
569        }
570    }
571
572    /// Validate that an integer is within range (for z-index)
573    fn validate_integer_range(
574        id: PropertyId,
575        value: &PropertyValue,
576        min: i32,
577        max: i32,
578    ) -> Result<()> {
579        match value {
580            PropertyValue::Integer(i) => {
581                if *i < min || *i > max {
582                    let error = ValidationError::new(
583                        id,
584                        format!("Integer value {} is out of range ({}-{})", i, min, max),
585                        Some(Self::get_range_suggestion(id, min as f64, max as f64)),
586                    );
587                    return Err(error.to_fop_error(value));
588                }
589                Ok(())
590            }
591            PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
592            _ => Err(FopError::PropertyValidation {
593                property: id.name().to_string(),
594                value: format!("{:?}", value),
595                reason: "Expected integer value".to_string(),
596            }),
597        }
598    }
599
600    /// Validate line-height property
601    fn validate_line_height(value: &PropertyValue) -> Result<()> {
602        match value {
603            PropertyValue::Length(len) => {
604                if len.to_pt() <= 0.0 {
605                    let error = ValidationError::new(
606                        PropertyId::LineHeight,
607                        format!("Line height {} must be positive", len.to_pt()),
608                        Some("Use a positive length, positive number, or 'normal'".to_string()),
609                    );
610                    return Err(error.to_fop_error(value));
611                }
612                Ok(())
613            }
614            PropertyValue::Number(n) => {
615                if *n <= 0.0 {
616                    let error = ValidationError::new(
617                        PropertyId::LineHeight,
618                        format!("Line height multiplier {} must be positive", n),
619                        Some("Use a positive number (e.g., 1.5 for 150% of font size)".to_string()),
620                    );
621                    return Err(error.to_fop_error(value));
622                }
623                Ok(())
624            }
625            PropertyValue::String(s) if s.as_ref() == "normal" => Ok(()),
626            PropertyValue::Percentage(p) => {
627                if p.as_fraction() <= 0.0 {
628                    let error = ValidationError::new(
629                        PropertyId::LineHeight,
630                        format!("Line height percentage {} must be positive", p),
631                        Some("Use a positive percentage (e.g., 150%)".to_string()),
632                    );
633                    return Err(error.to_fop_error(value));
634                }
635                Ok(())
636            }
637            PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
638            _ => {
639                let error = ValidationError::new(
640                    PropertyId::LineHeight,
641                    "Invalid line-height value".to_string(),
642                    Some("Use a positive length, positive number, or 'normal'".to_string()),
643                );
644                Err(error.to_fop_error(value))
645            }
646        }
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653    use fop_types::{Length, Percentage};
654
655    #[test]
656    fn test_opacity_range_validation() {
657        let validator = PropertyValidator::new();
658
659        // Valid opacity
660        let valid = PropertyValue::Number(0.5);
661        assert!(validator.validate(PropertyId::Opacity, &valid).is_ok());
662
663        // Invalid: too high
664        let invalid_high = PropertyValue::Number(1.5);
665        assert!(validator
666            .validate(PropertyId::Opacity, &invalid_high)
667            .is_err());
668
669        // Invalid: too low
670        let invalid_low = PropertyValue::Number(-0.1);
671        assert!(validator
672            .validate(PropertyId::Opacity, &invalid_low)
673            .is_err());
674
675        // Edge cases
676        let zero = PropertyValue::Number(0.0);
677        assert!(validator.validate(PropertyId::Opacity, &zero).is_ok());
678
679        let one = PropertyValue::Number(1.0);
680        assert!(validator.validate(PropertyId::Opacity, &one).is_ok());
681    }
682
683    #[test]
684    fn test_column_count_range_validation() {
685        let validator = PropertyValidator::new();
686
687        // Valid column count
688        let valid = PropertyValue::Integer(3);
689        assert!(validator.validate(PropertyId::ColumnCount, &valid).is_ok());
690
691        // Invalid: zero
692        let zero = PropertyValue::Integer(0);
693        assert!(validator.validate(PropertyId::ColumnCount, &zero).is_err());
694
695        // Invalid: negative
696        let negative = PropertyValue::Integer(-1);
697        assert!(validator
698            .validate(PropertyId::ColumnCount, &negative)
699            .is_err());
700
701        // Invalid: too high
702        let too_high = PropertyValue::Integer(300);
703        assert!(validator
704            .validate(PropertyId::ColumnCount, &too_high)
705            .is_err());
706
707        // Edge cases
708        let min = PropertyValue::Integer(1);
709        assert!(validator.validate(PropertyId::ColumnCount, &min).is_ok());
710
711        let max = PropertyValue::Integer(255);
712        assert!(validator.validate(PropertyId::ColumnCount, &max).is_ok());
713    }
714
715    #[test]
716    fn test_orphans_widows_validation() {
717        let validator = PropertyValidator::new();
718
719        // Valid values
720        let valid = PropertyValue::Integer(2);
721        assert!(validator.validate(PropertyId::Orphans, &valid).is_ok());
722        assert!(validator.validate(PropertyId::Widows, &valid).is_ok());
723
724        // Invalid: zero
725        let zero = PropertyValue::Integer(0);
726        assert!(validator.validate(PropertyId::Orphans, &zero).is_err());
727        assert!(validator.validate(PropertyId::Widows, &zero).is_err());
728
729        // Invalid: too high
730        let too_high = PropertyValue::Integer(1000);
731        assert!(validator.validate(PropertyId::Orphans, &too_high).is_err());
732        assert!(validator.validate(PropertyId::Widows, &too_high).is_err());
733
734        // Edge cases
735        let min = PropertyValue::Integer(1);
736        assert!(validator.validate(PropertyId::Orphans, &min).is_ok());
737        assert!(validator.validate(PropertyId::Widows, &min).is_ok());
738
739        let max = PropertyValue::Integer(999);
740        assert!(validator.validate(PropertyId::Orphans, &max).is_ok());
741        assert!(validator.validate(PropertyId::Widows, &max).is_ok());
742    }
743
744    #[test]
745    fn test_z_index_validation() {
746        let validator = PropertyValidator::new();
747
748        // Valid z-index values
749        let positive = PropertyValue::Integer(100);
750        assert!(validator.validate(PropertyId::ZIndex, &positive).is_ok());
751
752        let negative = PropertyValue::Integer(-50);
753        assert!(validator.validate(PropertyId::ZIndex, &negative).is_ok());
754
755        let zero = PropertyValue::Integer(0);
756        assert!(validator.validate(PropertyId::ZIndex, &zero).is_ok());
757
758        // Invalid: out of range
759        let too_high = PropertyValue::Integer(1_000_000);
760        assert!(validator.validate(PropertyId::ZIndex, &too_high).is_err());
761
762        let too_low = PropertyValue::Integer(-1_000_000);
763        assert!(validator.validate(PropertyId::ZIndex, &too_low).is_err());
764
765        // Edge cases
766        let min = PropertyValue::Integer(-999999);
767        assert!(validator.validate(PropertyId::ZIndex, &min).is_ok());
768
769        let max = PropertyValue::Integer(999999);
770        assert!(validator.validate(PropertyId::ZIndex, &max).is_ok());
771    }
772
773    #[test]
774    fn test_text_align_enum_validation() {
775        let validator = PropertyValidator::new();
776
777        // Valid values
778        let valid_values = vec!["start", "end", "center", "justify", "left", "right"];
779        for value_str in valid_values {
780            let value = PropertyValue::String(std::borrow::Cow::Borrowed(value_str));
781            assert!(validator.validate(PropertyId::TextAlign, &value).is_ok());
782        }
783
784        // Invalid value
785        let invalid = PropertyValue::String(std::borrow::Cow::Borrowed("invalid"));
786        assert!(validator.validate(PropertyId::TextAlign, &invalid).is_err());
787    }
788
789    #[test]
790    fn test_overflow_enum_validation() {
791        let validator = PropertyValidator::new();
792
793        // Valid values
794        let valid_values = vec!["visible", "hidden", "scroll", "auto"];
795        for value_str in valid_values {
796            let value = PropertyValue::String(std::borrow::Cow::Borrowed(value_str));
797            assert!(validator.validate(PropertyId::Overflow, &value).is_ok());
798        }
799
800        // Invalid value
801        let invalid = PropertyValue::String(std::borrow::Cow::Borrowed("clip"));
802        assert!(validator.validate(PropertyId::Overflow, &invalid).is_err());
803    }
804
805    #[test]
806    fn test_border_style_enum_validation() {
807        let validator = PropertyValidator::new();
808
809        // Valid values
810        let valid_values = vec![
811            "none", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset",
812        ];
813        for value_str in valid_values {
814            let value = PropertyValue::String(std::borrow::Cow::Borrowed(value_str));
815            assert!(validator.validate(PropertyId::BorderStyle, &value).is_ok());
816            assert!(validator
817                .validate(PropertyId::BorderTopStyle, &value)
818                .is_ok());
819            assert!(validator
820                .validate(PropertyId::BorderBottomStyle, &value)
821                .is_ok());
822        }
823
824        // Invalid value
825        let invalid = PropertyValue::String(std::borrow::Cow::Borrowed("wavy"));
826        assert!(validator
827            .validate(PropertyId::BorderStyle, &invalid)
828            .is_err());
829    }
830
831    #[test]
832    fn test_break_enum_validation() {
833        let validator = PropertyValidator::new();
834
835        // Valid values
836        let valid_values = vec!["auto", "column", "page", "even-page", "odd-page"];
837        for value_str in valid_values {
838            let value = PropertyValue::String(std::borrow::Cow::Borrowed(value_str));
839            assert!(validator.validate(PropertyId::BreakBefore, &value).is_ok());
840            assert!(validator.validate(PropertyId::BreakAfter, &value).is_ok());
841        }
842
843        // Invalid value
844        let invalid = PropertyValue::String(std::borrow::Cow::Borrowed("always"));
845        assert!(validator
846            .validate(PropertyId::BreakBefore, &invalid)
847            .is_err());
848    }
849
850    #[test]
851    fn test_font_size_positive_validation() {
852        let validator = PropertyValidator::new();
853
854        // Valid font size
855        let valid = PropertyValue::Length(Length::from_pt(12.0));
856        assert!(validator.validate(PropertyId::FontSize, &valid).is_ok());
857
858        // Invalid: negative
859        let invalid = PropertyValue::Length(Length::from_pt(-5.0));
860        assert!(validator.validate(PropertyId::FontSize, &invalid).is_err());
861
862        // Invalid: zero
863        let zero = PropertyValue::Length(Length::from_pt(0.0));
864        assert!(validator.validate(PropertyId::FontSize, &zero).is_err());
865    }
866
867    #[test]
868    fn test_line_height_validation() {
869        let validator = PropertyValidator::new();
870
871        // Valid: positive length
872        let valid_length = PropertyValue::Length(Length::from_pt(14.0));
873        assert!(validator
874            .validate(PropertyId::LineHeight, &valid_length)
875            .is_ok());
876
877        // Valid: positive number
878        let valid_number = PropertyValue::Number(1.5);
879        assert!(validator
880            .validate(PropertyId::LineHeight, &valid_number)
881            .is_ok());
882
883        // Valid: "normal"
884        let normal = PropertyValue::String(std::borrow::Cow::Borrowed("normal"));
885        assert!(validator.validate(PropertyId::LineHeight, &normal).is_ok());
886
887        // Invalid: zero length
888        let invalid_length = PropertyValue::Length(Length::from_pt(0.0));
889        assert!(validator
890            .validate(PropertyId::LineHeight, &invalid_length)
891            .is_err());
892
893        // Invalid: negative number
894        let invalid_number = PropertyValue::Number(-1.0);
895        assert!(validator
896            .validate(PropertyId::LineHeight, &invalid_number)
897            .is_err());
898    }
899
900    #[test]
901    fn test_auto_inherit_none_always_valid() {
902        let validator = PropertyValidator::new();
903
904        // Auto, Inherit, and None should always pass validation
905        assert!(validator
906            .validate(PropertyId::Opacity, &PropertyValue::Auto)
907            .is_ok());
908        assert!(validator
909            .validate(PropertyId::Opacity, &PropertyValue::Inherit)
910            .is_ok());
911        assert!(validator
912            .validate(PropertyId::Opacity, &PropertyValue::None)
913            .is_ok());
914
915        assert!(validator
916            .validate(PropertyId::ColumnCount, &PropertyValue::Auto)
917            .is_ok());
918        assert!(validator
919            .validate(PropertyId::FontSize, &PropertyValue::Inherit)
920            .is_ok());
921    }
922
923    #[test]
924    fn test_mutual_exclusion_width_ipd() {
925        let validator = PropertyValidator::new();
926
927        let props = vec![
928            (
929                PropertyId::Width,
930                PropertyValue::Length(Length::from_pt(100.0)),
931            ),
932            (
933                PropertyId::InlineProgressionDimension,
934                PropertyValue::Length(Length::from_pt(100.0)),
935            ),
936        ];
937
938        let errors = validator.check_mutual_exclusion(&props);
939        assert_eq!(errors.len(), 1);
940        assert_eq!(errors[0].property, PropertyId::Width);
941    }
942
943    #[test]
944    fn test_mutual_exclusion_height_bpd() {
945        let validator = PropertyValidator::new();
946
947        let props = vec![
948            (
949                PropertyId::Height,
950                PropertyValue::Length(Length::from_pt(200.0)),
951            ),
952            (
953                PropertyId::BlockProgressionDimension,
954                PropertyValue::Length(Length::from_pt(200.0)),
955            ),
956        ];
957
958        let errors = validator.check_mutual_exclusion(&props);
959        assert_eq!(errors.len(), 1);
960        assert_eq!(errors[0].property, PropertyId::Height);
961    }
962
963    #[test]
964    fn test_no_mutual_exclusion() {
965        let validator = PropertyValidator::new();
966
967        let props = vec![
968            (
969                PropertyId::Width,
970                PropertyValue::Length(Length::from_pt(100.0)),
971            ),
972            (
973                PropertyId::Height,
974                PropertyValue::Length(Length::from_pt(200.0)),
975            ),
976        ];
977
978        let errors = validator.check_mutual_exclusion(&props);
979        assert_eq!(errors.len(), 0);
980    }
981
982    #[test]
983    fn test_validation_error_to_fop_error() {
984        let error = ValidationError::new(
985            PropertyId::Opacity,
986            "Value out of range".to_string(),
987            Some("Use 0.0-1.0".to_string()),
988        );
989
990        let value = PropertyValue::Number(1.5);
991        let fop_error = error.to_fop_error(&value);
992
993        match fop_error {
994            FopError::PropertyValidation {
995                property,
996                value: _,
997                reason,
998            } => {
999                assert_eq!(property, "opacity");
1000                assert!(reason.contains("Value out of range"));
1001                assert!(reason.contains("Use 0.0-1.0"));
1002            }
1003            _ => panic!("Expected PropertyValidation error"),
1004        }
1005    }
1006
1007    #[test]
1008    fn test_length_range_validation() {
1009        let value = PropertyValue::Length(Length::from_pt(50.0));
1010        assert!(
1011            PropertyValidator::validate_length_range(PropertyId::FontSize, &value, 0.0, 100.0)
1012                .is_ok()
1013        );
1014
1015        assert!(PropertyValidator::validate_length_range(
1016            PropertyId::FontSize,
1017            &value,
1018            60.0,
1019            100.0
1020        )
1021        .is_err());
1022    }
1023
1024    #[test]
1025    fn test_number_range_validation() {
1026        let value = PropertyValue::Number(0.5);
1027        assert!(
1028            PropertyValidator::validate_number_range(PropertyId::Opacity, &value, 0.0, 1.0).is_ok()
1029        );
1030
1031        assert!(
1032            PropertyValidator::validate_number_range(PropertyId::Opacity, &value, 0.6, 1.0)
1033                .is_err()
1034        );
1035    }
1036
1037    #[test]
1038    fn test_percentage_range_validation() {
1039        let value = PropertyValue::Percentage(Percentage::new(0.5)); // 50%
1040        assert!(
1041            PropertyValidator::validate_percentage_range(PropertyId::Width, &value, 0.0, 1.0)
1042                .is_ok()
1043        );
1044
1045        assert!(
1046            PropertyValidator::validate_percentage_range(PropertyId::Width, &value, 0.6, 1.0)
1047                .is_err()
1048        );
1049    }
1050
1051    #[test]
1052    fn test_enum_validation_legacy() {
1053        let value = PropertyValue::Enum(1);
1054        let allowed = vec![1, 2, 3];
1055        assert!(PropertyValidator::validate_enum(PropertyId::FontStyle, &value, &allowed).is_ok());
1056
1057        let invalid = PropertyValue::Enum(5);
1058        assert!(
1059            PropertyValidator::validate_enum(PropertyId::FontStyle, &invalid, &allowed).is_err()
1060        );
1061    }
1062
1063    #[test]
1064    fn test_validator_instance_vs_static() {
1065        // Test that both instance and static methods work
1066        let validator = PropertyValidator::new();
1067
1068        let valid_opacity = PropertyValue::Number(0.5);
1069        assert!(validator
1070            .validate(PropertyId::Opacity, &valid_opacity)
1071            .is_ok());
1072        assert!(PropertyValidator::validate_static(PropertyId::Opacity, &valid_opacity).is_ok());
1073
1074        let invalid_opacity = PropertyValue::Number(1.5);
1075        assert!(validator
1076            .validate(PropertyId::Opacity, &invalid_opacity)
1077            .is_err());
1078        assert!(PropertyValidator::validate_static(PropertyId::Opacity, &invalid_opacity).is_err());
1079    }
1080}
1081
1082#[cfg(test)]
1083mod tests_extended {
1084    use super::*;
1085    use fop_types::{Length, Percentage};
1086
1087    // -----------------------------------------------------------------------
1088    // validate_length_range
1089    // -----------------------------------------------------------------------
1090
1091    #[test]
1092    fn test_length_range_exactly_at_min() {
1093        let v = PropertyValue::Length(Length::from_pt(0.0));
1094        assert!(
1095            PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_ok()
1096        );
1097    }
1098
1099    #[test]
1100    fn test_length_range_exactly_at_max() {
1101        let v = PropertyValue::Length(Length::from_pt(100.0));
1102        assert!(
1103            PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_ok()
1104        );
1105    }
1106
1107    #[test]
1108    fn test_length_range_below_min() {
1109        let v = PropertyValue::Length(Length::from_pt(-1.0));
1110        assert!(
1111            PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_err()
1112        );
1113    }
1114
1115    #[test]
1116    fn test_length_range_above_max() {
1117        let v = PropertyValue::Length(Length::from_pt(101.0));
1118        assert!(
1119            PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_err()
1120        );
1121    }
1122
1123    #[test]
1124    fn test_length_range_auto_passes() {
1125        assert!(PropertyValidator::validate_length_range(
1126            PropertyId::Width,
1127            &PropertyValue::Auto,
1128            0.0,
1129            100.0
1130        )
1131        .is_ok());
1132    }
1133
1134    #[test]
1135    fn test_length_range_inherit_passes() {
1136        assert!(PropertyValidator::validate_length_range(
1137            PropertyId::Width,
1138            &PropertyValue::Inherit,
1139            0.0,
1140            100.0
1141        )
1142        .is_ok());
1143    }
1144
1145    #[test]
1146    fn test_length_range_none_passes() {
1147        assert!(PropertyValidator::validate_length_range(
1148            PropertyId::Width,
1149            &PropertyValue::None,
1150            0.0,
1151            100.0
1152        )
1153        .is_ok());
1154    }
1155
1156    #[test]
1157    fn test_length_range_rejects_non_length() {
1158        let v = PropertyValue::Number(50.0);
1159        assert!(
1160            PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_err()
1161        );
1162    }
1163
1164    // -----------------------------------------------------------------------
1165    // validate_number_range
1166    // -----------------------------------------------------------------------
1167
1168    #[test]
1169    fn test_number_range_exactly_at_boundaries() {
1170        assert!(PropertyValidator::validate_number_range(
1171            PropertyId::Opacity,
1172            &PropertyValue::Number(0.0),
1173            0.0,
1174            1.0
1175        )
1176        .is_ok());
1177        assert!(PropertyValidator::validate_number_range(
1178            PropertyId::Opacity,
1179            &PropertyValue::Number(1.0),
1180            0.0,
1181            1.0
1182        )
1183        .is_ok());
1184    }
1185
1186    #[test]
1187    fn test_number_range_outside_boundaries() {
1188        assert!(PropertyValidator::validate_number_range(
1189            PropertyId::Opacity,
1190            &PropertyValue::Number(-0.001),
1191            0.0,
1192            1.0
1193        )
1194        .is_err());
1195        assert!(PropertyValidator::validate_number_range(
1196            PropertyId::Opacity,
1197            &PropertyValue::Number(1.001),
1198            0.0,
1199            1.0
1200        )
1201        .is_err());
1202    }
1203
1204    #[test]
1205    fn test_number_range_integer_value() {
1206        // Integer values are accepted as numbers
1207        let v = PropertyValue::Integer(1);
1208        assert!(
1209            PropertyValidator::validate_number_range(PropertyId::Opacity, &v, 0.0, 2.0).is_ok()
1210        );
1211    }
1212
1213    #[test]
1214    fn test_number_range_auto_passes() {
1215        assert!(PropertyValidator::validate_number_range(
1216            PropertyId::Opacity,
1217            &PropertyValue::Auto,
1218            0.0,
1219            1.0
1220        )
1221        .is_ok());
1222    }
1223
1224    #[test]
1225    fn test_number_range_rejects_string() {
1226        let v = PropertyValue::String(std::borrow::Cow::Borrowed("0.5"));
1227        assert!(
1228            PropertyValidator::validate_number_range(PropertyId::Opacity, &v, 0.0, 1.0).is_err()
1229        );
1230    }
1231
1232    // -----------------------------------------------------------------------
1233    // validate_percentage_range
1234    // -----------------------------------------------------------------------
1235
1236    #[test]
1237    fn test_percentage_range_valid() {
1238        let v = PropertyValue::Percentage(Percentage::new(0.5)); // 50%
1239        assert!(
1240            PropertyValidator::validate_percentage_range(PropertyId::Width, &v, 0.0, 1.0).is_ok()
1241        );
1242    }
1243
1244    #[test]
1245    fn test_percentage_range_too_high() {
1246        let v = PropertyValue::Percentage(Percentage::new(1.5)); // 150%
1247        assert!(
1248            PropertyValidator::validate_percentage_range(PropertyId::Width, &v, 0.0, 1.0).is_err()
1249        );
1250    }
1251
1252    #[test]
1253    fn test_percentage_range_too_low() {
1254        let v = PropertyValue::Percentage(Percentage::new(-0.1));
1255        assert!(
1256            PropertyValidator::validate_percentage_range(PropertyId::Width, &v, 0.0, 1.0).is_err()
1257        );
1258    }
1259
1260    #[test]
1261    fn test_percentage_range_auto_passes() {
1262        assert!(PropertyValidator::validate_percentage_range(
1263            PropertyId::Width,
1264            &PropertyValue::Auto,
1265            0.0,
1266            1.0
1267        )
1268        .is_ok());
1269    }
1270
1271    // -----------------------------------------------------------------------
1272    // validate_enum (legacy u16 interface)
1273    // -----------------------------------------------------------------------
1274
1275    #[test]
1276    fn test_enum_valid_value() {
1277        let v = PropertyValue::Enum(2);
1278        assert!(PropertyValidator::validate_enum(PropertyId::FontStyle, &v, &[1, 2, 3]).is_ok());
1279    }
1280
1281    #[test]
1282    fn test_enum_invalid_value() {
1283        let v = PropertyValue::Enum(99);
1284        assert!(PropertyValidator::validate_enum(PropertyId::FontStyle, &v, &[1, 2, 3]).is_err());
1285    }
1286
1287    #[test]
1288    fn test_enum_wrong_type_rejected() {
1289        let v = PropertyValue::String(std::borrow::Cow::Borrowed("normal"));
1290        assert!(PropertyValidator::validate_enum(PropertyId::FontStyle, &v, &[1, 2, 3]).is_err());
1291    }
1292
1293    // -----------------------------------------------------------------------
1294    // validate()/validate_static() for font-size positive-length
1295    // -----------------------------------------------------------------------
1296
1297    #[test]
1298    fn test_font_size_mm_valid() {
1299        let v = PropertyValue::Length(Length::from_mm(5.0));
1300        let validator = PropertyValidator::new();
1301        assert!(validator.validate(PropertyId::FontSize, &v).is_ok());
1302    }
1303
1304    #[test]
1305    fn test_font_size_negative_rejected() {
1306        let v = PropertyValue::Length(Length::from_pt(-1.0));
1307        assert!(PropertyValidator::validate_static(PropertyId::FontSize, &v).is_err());
1308    }
1309
1310    #[test]
1311    fn test_font_size_zero_rejected() {
1312        let v = PropertyValue::Length(Length::from_pt(0.0));
1313        assert!(PropertyValidator::validate_static(PropertyId::FontSize, &v).is_err());
1314    }
1315
1316    #[test]
1317    fn test_font_size_percentage_passes() {
1318        let v = PropertyValue::Percentage(Percentage::new(1.2)); // 120%
1319        let validator = PropertyValidator::new();
1320        assert!(validator.validate(PropertyId::FontSize, &v).is_ok());
1321    }
1322
1323    #[test]
1324    fn test_font_size_inherit_passes() {
1325        let validator = PropertyValidator::new();
1326        assert!(validator
1327            .validate(PropertyId::FontSize, &PropertyValue::Inherit)
1328            .is_ok());
1329    }
1330
1331    // -----------------------------------------------------------------------
1332    // line-height validation
1333    // -----------------------------------------------------------------------
1334
1335    #[test]
1336    fn test_line_height_zero_length_rejected() {
1337        let v = PropertyValue::Length(Length::from_pt(0.0));
1338        let validator = PropertyValidator::new();
1339        assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1340    }
1341
1342    #[test]
1343    fn test_line_height_negative_length_rejected() {
1344        let v = PropertyValue::Length(Length::from_pt(-5.0));
1345        let validator = PropertyValidator::new();
1346        assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1347    }
1348
1349    #[test]
1350    fn test_line_height_zero_number_rejected() {
1351        let v = PropertyValue::Number(0.0);
1352        let validator = PropertyValidator::new();
1353        assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1354    }
1355
1356    #[test]
1357    fn test_line_height_negative_number_rejected() {
1358        let v = PropertyValue::Number(-1.5);
1359        let validator = PropertyValidator::new();
1360        assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1361    }
1362
1363    #[test]
1364    fn test_line_height_percentage_valid() {
1365        let v = PropertyValue::Percentage(Percentage::new(1.5)); // 150%
1366        let validator = PropertyValidator::new();
1367        assert!(validator.validate(PropertyId::LineHeight, &v).is_ok());
1368    }
1369
1370    #[test]
1371    fn test_line_height_invalid_string() {
1372        let v = PropertyValue::String(std::borrow::Cow::Borrowed("tight"));
1373        let validator = PropertyValidator::new();
1374        assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1375    }
1376
1377    // -----------------------------------------------------------------------
1378    // opacity range via validate_static
1379    // -----------------------------------------------------------------------
1380
1381    #[test]
1382    fn test_static_opacity_edge_values() {
1383        assert!(PropertyValidator::validate_static(
1384            PropertyId::Opacity,
1385            &PropertyValue::Number(0.0)
1386        )
1387        .is_ok());
1388        assert!(PropertyValidator::validate_static(
1389            PropertyId::Opacity,
1390            &PropertyValue::Number(1.0)
1391        )
1392        .is_ok());
1393    }
1394
1395    #[test]
1396    fn test_static_opacity_out_of_range() {
1397        assert!(PropertyValidator::validate_static(
1398            PropertyId::Opacity,
1399            &PropertyValue::Number(1.01)
1400        )
1401        .is_err());
1402        assert!(PropertyValidator::validate_static(
1403            PropertyId::Opacity,
1404            &PropertyValue::Number(-0.01)
1405        )
1406        .is_err());
1407    }
1408
1409    // -----------------------------------------------------------------------
1410    // column-count via validate_static
1411    // -----------------------------------------------------------------------
1412
1413    #[test]
1414    fn test_static_column_count_boundaries() {
1415        assert!(PropertyValidator::validate_static(
1416            PropertyId::ColumnCount,
1417            &PropertyValue::Integer(1)
1418        )
1419        .is_ok());
1420        assert!(PropertyValidator::validate_static(
1421            PropertyId::ColumnCount,
1422            &PropertyValue::Integer(255)
1423        )
1424        .is_ok());
1425        assert!(PropertyValidator::validate_static(
1426            PropertyId::ColumnCount,
1427            &PropertyValue::Integer(0)
1428        )
1429        .is_err());
1430        assert!(PropertyValidator::validate_static(
1431            PropertyId::ColumnCount,
1432            &PropertyValue::Integer(256)
1433        )
1434        .is_err());
1435    }
1436
1437    // -----------------------------------------------------------------------
1438    // widows / orphans via validate_static
1439    // -----------------------------------------------------------------------
1440
1441    #[test]
1442    fn test_static_widows_orphans_boundaries() {
1443        assert!(
1444            PropertyValidator::validate_static(PropertyId::Widows, &PropertyValue::Integer(1))
1445                .is_ok()
1446        );
1447        assert!(PropertyValidator::validate_static(
1448            PropertyId::Widows,
1449            &PropertyValue::Integer(999)
1450        )
1451        .is_ok());
1452        assert!(
1453            PropertyValidator::validate_static(PropertyId::Widows, &PropertyValue::Integer(0))
1454                .is_err()
1455        );
1456        assert!(PropertyValidator::validate_static(
1457            PropertyId::Orphans,
1458            &PropertyValue::Integer(1000)
1459        )
1460        .is_err());
1461    }
1462
1463    // -----------------------------------------------------------------------
1464    // z-index via validate_static
1465    // -----------------------------------------------------------------------
1466
1467    #[test]
1468    fn test_static_z_index_boundaries() {
1469        assert!(PropertyValidator::validate_static(
1470            PropertyId::ZIndex,
1471            &PropertyValue::Integer(-999999)
1472        )
1473        .is_ok());
1474        assert!(PropertyValidator::validate_static(
1475            PropertyId::ZIndex,
1476            &PropertyValue::Integer(999999)
1477        )
1478        .is_ok());
1479        assert!(PropertyValidator::validate_static(
1480            PropertyId::ZIndex,
1481            &PropertyValue::Integer(1_000_000)
1482        )
1483        .is_err());
1484        assert!(PropertyValidator::validate_static(
1485            PropertyId::ZIndex,
1486            &PropertyValue::Integer(-1_000_000)
1487        )
1488        .is_err());
1489    }
1490
1491    // -----------------------------------------------------------------------
1492    // ValidationError struct
1493    // -----------------------------------------------------------------------
1494
1495    #[test]
1496    fn test_validation_error_no_suggestion() {
1497        let err = ValidationError::new(PropertyId::Opacity, "Bad value".to_string(), None);
1498        let v = PropertyValue::Number(99.0);
1499        let fop_err = err.to_fop_error(&v);
1500        match fop_err {
1501            FopError::PropertyValidation { reason, .. } => {
1502                assert!(reason.contains("Bad value"));
1503                assert!(!reason.contains("Suggestion:"));
1504            }
1505            _ => panic!("Expected PropertyValidation error"),
1506        }
1507    }
1508
1509    #[test]
1510    fn test_validation_error_with_suggestion() {
1511        let err = ValidationError::new(
1512            PropertyId::Opacity,
1513            "Out of range".to_string(),
1514            Some("Use 0.0 to 1.0".to_string()),
1515        );
1516        let v = PropertyValue::Number(99.0);
1517        let fop_err = err.to_fop_error(&v);
1518        match fop_err {
1519            FopError::PropertyValidation { reason, .. } => {
1520                assert!(reason.contains("Out of range"));
1521                assert!(reason.contains("Use 0.0 to 1.0"));
1522            }
1523            _ => panic!("Expected PropertyValidation error"),
1524        }
1525    }
1526
1527    #[test]
1528    fn test_validation_error_equality() {
1529        let e1 = ValidationError::new(PropertyId::Opacity, "msg".to_string(), None);
1530        let e2 = ValidationError::new(PropertyId::Opacity, "msg".to_string(), None);
1531        let e3 = ValidationError::new(PropertyId::Width, "msg".to_string(), None);
1532        assert_eq!(e1, e2);
1533        assert_ne!(e1, e3);
1534    }
1535
1536    // -----------------------------------------------------------------------
1537    // ValidationRule enum
1538    // -----------------------------------------------------------------------
1539
1540    #[test]
1541    fn test_validation_rule_range_clone() {
1542        let rule = ValidationRule::Range { min: 0.0, max: 1.0 };
1543        let cloned = rule.clone();
1544        assert_eq!(rule, cloned);
1545    }
1546
1547    #[test]
1548    fn test_validation_rule_string_enum_clone() {
1549        let rule = ValidationRule::StringEnum {
1550            allowed: vec!["a", "b"],
1551        };
1552        let cloned = rule.clone();
1553        assert_eq!(rule, cloned);
1554    }
1555
1556    // -----------------------------------------------------------------------
1557    // PropertyValidator with custom rules
1558    // -----------------------------------------------------------------------
1559
1560    #[test]
1561    fn test_custom_rule_overrides_for_arbitrary_property() {
1562        let mut validator = PropertyValidator::new();
1563        // Add a range rule for Width: 1pt - 500pt
1564        validator.add_rule(
1565            PropertyId::Width,
1566            ValidationRule::Range {
1567                min: 1.0,
1568                max: 500.0,
1569            },
1570        );
1571        assert!(validator
1572            .validate(
1573                PropertyId::Width,
1574                &PropertyValue::Length(Length::from_pt(100.0))
1575            )
1576            .is_ok());
1577        assert!(validator
1578            .validate(
1579                PropertyId::Width,
1580                &PropertyValue::Length(Length::from_pt(0.0))
1581            )
1582            .is_err());
1583    }
1584
1585    #[test]
1586    fn test_unknown_property_passes_with_any_value() {
1587        let validator = PropertyValidator::new();
1588        // A property with no registered rules always passes validation
1589        let v = PropertyValue::Length(Length::from_pt(42.0));
1590        // SpaceBefore has no registered range rule
1591        assert!(validator.validate(PropertyId::SpaceBefore, &v).is_ok());
1592    }
1593
1594    // -----------------------------------------------------------------------
1595    // mutual exclusion: both absent
1596    // -----------------------------------------------------------------------
1597
1598    #[test]
1599    fn test_no_mutual_exclusion_when_neither_present() {
1600        let validator = PropertyValidator::new();
1601        let props: Vec<(PropertyId, PropertyValue)> = vec![];
1602        let errors = validator.check_mutual_exclusion(&props);
1603        assert!(errors.is_empty());
1604    }
1605
1606    #[test]
1607    fn test_mutual_exclusion_only_width() {
1608        let validator = PropertyValidator::new();
1609        let props = vec![(
1610            PropertyId::Width,
1611            PropertyValue::Length(Length::from_pt(100.0)),
1612        )];
1613        let errors = validator.check_mutual_exclusion(&props);
1614        assert!(errors.is_empty());
1615    }
1616
1617    #[test]
1618    fn test_mutual_exclusion_only_height() {
1619        let validator = PropertyValidator::new();
1620        let props = vec![(
1621            PropertyId::Height,
1622            PropertyValue::Length(Length::from_pt(100.0)),
1623        )];
1624        let errors = validator.check_mutual_exclusion(&props);
1625        assert!(errors.is_empty());
1626    }
1627}