presentar_widgets/
model_card.rs

1//! `ModelCard` widget for displaying ML model 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/// Model status indicator.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub enum ModelStatus {
16    /// Draft/in development
17    #[default]
18    Draft,
19    /// Under review
20    Review,
21    /// Published/production ready
22    Published,
23    /// Deprecated
24    Deprecated,
25    /// Archived
26    Archived,
27}
28
29impl ModelStatus {
30    /// Get display color for status.
31    #[must_use]
32    pub fn color(&self) -> Color {
33        match self {
34            Self::Draft => Color::new(0.6, 0.6, 0.6, 1.0),
35            Self::Review => Color::new(0.9, 0.7, 0.1, 1.0),
36            Self::Published => Color::new(0.2, 0.7, 0.3, 1.0),
37            Self::Deprecated => Color::new(0.9, 0.5, 0.1, 1.0),
38            Self::Archived => Color::new(0.5, 0.5, 0.5, 1.0),
39        }
40    }
41
42    /// Get status label.
43    #[must_use]
44    pub const fn label(&self) -> &'static str {
45        match self {
46            Self::Draft => "Draft",
47            Self::Review => "Review",
48            Self::Published => "Published",
49            Self::Deprecated => "Deprecated",
50            Self::Archived => "Archived",
51        }
52    }
53}
54
55/// Model metric (e.g., accuracy, F1 score).
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct ModelMetric {
58    /// Metric name
59    pub name: String,
60    /// Metric value
61    pub value: f64,
62    /// Optional unit
63    pub unit: Option<String>,
64    /// Higher is better
65    pub higher_is_better: bool,
66}
67
68impl ModelMetric {
69    /// Create a new metric.
70    #[must_use]
71    pub fn new(name: impl Into<String>, value: f64) -> Self {
72        Self {
73            name: name.into(),
74            value,
75            unit: None,
76            higher_is_better: true,
77        }
78    }
79
80    /// Set unit.
81    #[must_use]
82    pub fn unit(mut self, unit: impl Into<String>) -> Self {
83        self.unit = Some(unit.into());
84        self
85    }
86
87    /// Set lower is better.
88    #[must_use]
89    pub const fn lower_is_better(mut self) -> Self {
90        self.higher_is_better = false;
91        self
92    }
93
94    /// Format the value for display.
95    #[must_use]
96    pub fn formatted_value(&self) -> String {
97        if let Some(ref unit) = self.unit {
98            format!("{:.2}{}", self.value, unit)
99        } else if self.value.abs() < 1.0 {
100            format!("{:.2}%", self.value * 100.0)
101        } else {
102            format!("{:.2}", self.value)
103        }
104    }
105}
106
107/// `ModelCard` widget for displaying ML model metadata.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ModelCard {
110    /// Model name
111    name: String,
112    /// Model version
113    version: String,
114    /// Model description
115    description: Option<String>,
116    /// Model status
117    status: ModelStatus,
118    /// Model type/framework (e.g., `PyTorch`, `TensorFlow`)
119    framework: Option<String>,
120    /// Model task (e.g., "classification", "regression")
121    task: Option<String>,
122    /// Performance metrics
123    metrics: Vec<ModelMetric>,
124    /// Parameter count
125    parameters: Option<u64>,
126    /// Training dataset name
127    dataset: Option<String>,
128    /// Author/owner
129    author: Option<String>,
130    /// Tags
131    tags: Vec<String>,
132    /// Custom metadata
133    metadata: HashMap<String, String>,
134    /// Card width
135    width: Option<f32>,
136    /// Card height
137    height: Option<f32>,
138    /// Background color
139    background: Color,
140    /// Border color
141    border_color: Color,
142    /// Corner radius
143    corner_radius: f32,
144    /// Show metrics chart
145    show_metrics_chart: bool,
146    /// Accessible name
147    accessible_name_value: Option<String>,
148    /// Test ID
149    test_id_value: Option<String>,
150    /// Cached bounds
151    #[serde(skip)]
152    bounds: Rect,
153}
154
155impl Default for ModelCard {
156    fn default() -> Self {
157        Self {
158            name: String::new(),
159            version: String::from("1.0.0"),
160            description: None,
161            status: ModelStatus::Draft,
162            framework: None,
163            task: None,
164            metrics: Vec::new(),
165            parameters: None,
166            dataset: None,
167            author: None,
168            tags: Vec::new(),
169            metadata: HashMap::new(),
170            width: None,
171            height: None,
172            background: Color::WHITE,
173            border_color: Color::new(0.9, 0.9, 0.9, 1.0),
174            corner_radius: 8.0,
175            show_metrics_chart: true,
176            accessible_name_value: None,
177            test_id_value: None,
178            bounds: Rect::default(),
179        }
180    }
181}
182
183impl ModelCard {
184    /// Create a new model card.
185    #[must_use]
186    pub fn new(name: impl Into<String>) -> Self {
187        Self {
188            name: name.into(),
189            ..Self::default()
190        }
191    }
192
193    /// Set model name.
194    #[must_use]
195    pub fn name(mut self, name: impl Into<String>) -> Self {
196        self.name = name.into();
197        self
198    }
199
200    /// Set version.
201    #[must_use]
202    pub fn version(mut self, version: impl Into<String>) -> Self {
203        self.version = version.into();
204        self
205    }
206
207    /// Set description.
208    #[must_use]
209    pub fn description(mut self, desc: impl Into<String>) -> Self {
210        self.description = Some(desc.into());
211        self
212    }
213
214    /// Set status.
215    #[must_use]
216    pub const fn status(mut self, status: ModelStatus) -> Self {
217        self.status = status;
218        self
219    }
220
221    /// Set framework.
222    #[must_use]
223    pub fn framework(mut self, framework: impl Into<String>) -> Self {
224        self.framework = Some(framework.into());
225        self
226    }
227
228    /// Set task.
229    #[must_use]
230    pub fn task(mut self, task: impl Into<String>) -> Self {
231        self.task = Some(task.into());
232        self
233    }
234
235    /// Add a metric.
236    #[must_use]
237    pub fn metric(mut self, metric: ModelMetric) -> Self {
238        self.metrics.push(metric);
239        self
240    }
241
242    /// Add multiple metrics.
243    #[must_use]
244    pub fn metrics(mut self, metrics: impl IntoIterator<Item = ModelMetric>) -> Self {
245        self.metrics.extend(metrics);
246        self
247    }
248
249    /// Set parameter count.
250    #[must_use]
251    pub const fn parameters(mut self, count: u64) -> Self {
252        self.parameters = Some(count);
253        self
254    }
255
256    /// Set dataset.
257    #[must_use]
258    pub fn dataset(mut self, dataset: impl Into<String>) -> Self {
259        self.dataset = Some(dataset.into());
260        self
261    }
262
263    /// Set author.
264    #[must_use]
265    pub fn author(mut self, author: impl Into<String>) -> Self {
266        self.author = Some(author.into());
267        self
268    }
269
270    /// Add a tag.
271    #[must_use]
272    pub fn tag(mut self, tag: impl Into<String>) -> Self {
273        self.tags.push(tag.into());
274        self
275    }
276
277    /// Add multiple tags.
278    #[must_use]
279    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
280        self.tags.extend(tags.into_iter().map(Into::into));
281        self
282    }
283
284    /// Add custom metadata.
285    #[must_use]
286    pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
287        self.metadata.insert(key.into(), value.into());
288        self
289    }
290
291    /// Set width.
292    #[must_use]
293    pub fn width(mut self, width: f32) -> Self {
294        self.width = Some(width.max(200.0));
295        self
296    }
297
298    /// Set height.
299    #[must_use]
300    pub fn height(mut self, height: f32) -> Self {
301        self.height = Some(height.max(150.0));
302        self
303    }
304
305    /// Set background color.
306    #[must_use]
307    pub const fn background(mut self, color: Color) -> Self {
308        self.background = color;
309        self
310    }
311
312    /// Set border color.
313    #[must_use]
314    pub const fn border_color(mut self, color: Color) -> Self {
315        self.border_color = color;
316        self
317    }
318
319    /// Set corner radius.
320    #[must_use]
321    pub fn corner_radius(mut self, radius: f32) -> Self {
322        self.corner_radius = radius.max(0.0);
323        self
324    }
325
326    /// Set whether to show metrics chart.
327    #[must_use]
328    pub const fn show_metrics_chart(mut self, show: bool) -> Self {
329        self.show_metrics_chart = show;
330        self
331    }
332
333    /// Set accessible name.
334    #[must_use]
335    pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
336        self.accessible_name_value = Some(name.into());
337        self
338    }
339
340    /// Set test ID.
341    #[must_use]
342    pub fn test_id(mut self, id: impl Into<String>) -> Self {
343        self.test_id_value = Some(id.into());
344        self
345    }
346
347    // Getters
348
349    /// Get model name.
350    #[must_use]
351    pub fn get_name(&self) -> &str {
352        &self.name
353    }
354
355    /// Get version.
356    #[must_use]
357    pub fn get_version(&self) -> &str {
358        &self.version
359    }
360
361    /// Get description.
362    #[must_use]
363    pub fn get_description(&self) -> Option<&str> {
364        self.description.as_deref()
365    }
366
367    /// Get status.
368    #[must_use]
369    pub const fn get_status(&self) -> ModelStatus {
370        self.status
371    }
372
373    /// Get framework.
374    #[must_use]
375    pub fn get_framework(&self) -> Option<&str> {
376        self.framework.as_deref()
377    }
378
379    /// Get task.
380    #[must_use]
381    pub fn get_task(&self) -> Option<&str> {
382        self.task.as_deref()
383    }
384
385    /// Get metrics.
386    #[must_use]
387    pub fn get_metrics(&self) -> &[ModelMetric] {
388        &self.metrics
389    }
390
391    /// Get parameter count.
392    #[must_use]
393    pub const fn get_parameters(&self) -> Option<u64> {
394        self.parameters
395    }
396
397    /// Get dataset.
398    #[must_use]
399    pub fn get_dataset(&self) -> Option<&str> {
400        self.dataset.as_deref()
401    }
402
403    /// Get author.
404    #[must_use]
405    pub fn get_author(&self) -> Option<&str> {
406        self.author.as_deref()
407    }
408
409    /// Get tags.
410    #[must_use]
411    pub fn get_tags(&self) -> &[String] {
412        &self.tags
413    }
414
415    /// Get custom metadata.
416    #[must_use]
417    pub fn get_metadata(&self, key: &str) -> Option<&str> {
418        self.metadata.get(key).map(String::as_str)
419    }
420
421    /// Check if model has metrics.
422    #[must_use]
423    pub fn has_metrics(&self) -> bool {
424        !self.metrics.is_empty()
425    }
426
427    /// Format parameter count for display.
428    #[must_use]
429    pub fn formatted_parameters(&self) -> Option<String> {
430        self.parameters.map(|p| {
431            if p >= 1_000_000_000 {
432                format!("{:.1}B", p as f64 / 1_000_000_000.0)
433            } else if p >= 1_000_000 {
434                format!("{:.1}M", p as f64 / 1_000_000.0)
435            } else if p >= 1_000 {
436                format!("{:.1}K", p as f64 / 1_000.0)
437            } else {
438                format!("{p}")
439            }
440        })
441    }
442}
443
444impl Widget for ModelCard {
445    fn type_id(&self) -> TypeId {
446        TypeId::of::<Self>()
447    }
448
449    fn measure(&self, constraints: Constraints) -> Size {
450        let width = self.width.unwrap_or(320.0);
451        let height = self.height.unwrap_or(200.0);
452        constraints.constrain(Size::new(width, height))
453    }
454
455    fn layout(&mut self, bounds: Rect) -> LayoutResult {
456        self.bounds = bounds;
457        LayoutResult {
458            size: bounds.size(),
459        }
460    }
461
462    #[allow(clippy::too_many_lines)]
463    fn paint(&self, canvas: &mut dyn Canvas) {
464        let padding = 16.0;
465
466        // Background
467        canvas.fill_rect(self.bounds, self.background);
468
469        // Border
470        canvas.stroke_rect(self.bounds, self.border_color, 1.0);
471
472        // Status badge
473        let status_color = self.status.color();
474        let badge_rect = Rect::new(
475            self.bounds.x + self.bounds.width - 80.0,
476            self.bounds.y + padding,
477            70.0,
478            22.0,
479        );
480        canvas.fill_rect(badge_rect, status_color);
481
482        let badge_style = TextStyle {
483            size: 10.0,
484            color: Color::WHITE,
485            ..TextStyle::default()
486        };
487        canvas.draw_text(
488            self.status.label(),
489            Point::new(badge_rect.x + 10.0, badge_rect.y + 15.0),
490            &badge_style,
491        );
492
493        // Title
494        let title_style = TextStyle {
495            size: 18.0,
496            color: Color::new(0.1, 0.1, 0.1, 1.0),
497            ..TextStyle::default()
498        };
499        canvas.draw_text(
500            &self.name,
501            Point::new(self.bounds.x + padding, self.bounds.y + padding + 16.0),
502            &title_style,
503        );
504
505        // Version
506        let version_style = TextStyle {
507            size: 12.0,
508            color: Color::new(0.5, 0.5, 0.5, 1.0),
509            ..TextStyle::default()
510        };
511        canvas.draw_text(
512            &format!("v{}", self.version),
513            Point::new(self.bounds.x + padding, self.bounds.y + padding + 36.0),
514            &version_style,
515        );
516
517        // Description (if any)
518        let mut y_offset = padding + 50.0;
519        if let Some(ref desc) = self.description {
520            let desc_style = TextStyle {
521                size: 12.0,
522                color: Color::new(0.3, 0.3, 0.3, 1.0),
523                ..TextStyle::default()
524            };
525            canvas.draw_text(
526                desc,
527                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
528                &desc_style,
529            );
530            y_offset += 24.0;
531        }
532
533        // Framework and task
534        if self.framework.is_some() || self.task.is_some() {
535            let info_style = TextStyle {
536                size: 11.0,
537                color: Color::new(0.4, 0.4, 0.4, 1.0),
538                ..TextStyle::default()
539            };
540            let info_text = match (&self.framework, &self.task) {
541                (Some(f), Some(t)) => format!("{f} • {t}"),
542                (Some(f), None) => f.clone(),
543                (None, Some(t)) => t.clone(),
544                (None, None) => String::new(),
545            };
546            canvas.draw_text(
547                &info_text,
548                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
549                &info_style,
550            );
551            y_offset += 20.0;
552        }
553
554        // Parameters
555        if let Some(params) = self.formatted_parameters() {
556            let params_style = TextStyle {
557                size: 11.0,
558                color: Color::new(0.4, 0.4, 0.4, 1.0),
559                ..TextStyle::default()
560            };
561            canvas.draw_text(
562                &format!("Parameters: {params}"),
563                Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
564                &params_style,
565            );
566            y_offset += 18.0;
567        }
568
569        // Metrics
570        if self.show_metrics_chart && !self.metrics.is_empty() {
571            let metric_style = TextStyle {
572                size: 11.0,
573                color: Color::new(0.2, 0.2, 0.2, 1.0),
574                ..TextStyle::default()
575            };
576            let value_style = TextStyle {
577                size: 14.0,
578                color: Color::new(0.2, 0.47, 0.96, 1.0),
579                ..TextStyle::default()
580            };
581
582            let metric_width =
583                (self.bounds.width - padding * 2.0) / self.metrics.len().min(4) as f32;
584            for (i, metric) in self.metrics.iter().take(4).enumerate() {
585                let mx = (i as f32).mul_add(metric_width, self.bounds.x + padding);
586                canvas.draw_text(
587                    &metric.name,
588                    Point::new(mx, self.bounds.y + y_offset + 12.0),
589                    &metric_style,
590                );
591                canvas.draw_text(
592                    &metric.formatted_value(),
593                    Point::new(mx, self.bounds.y + y_offset + 28.0),
594                    &value_style,
595                );
596            }
597            y_offset += 40.0;
598        }
599
600        // Tags
601        if !self.tags.is_empty() {
602            let tag_style = TextStyle {
603                size: 10.0,
604                color: Color::new(0.3, 0.3, 0.3, 1.0),
605                ..TextStyle::default()
606            };
607            let tag_bg = Color::new(0.95, 0.95, 0.95, 1.0);
608
609            let mut tx = self.bounds.x + padding;
610            for tag in self.tags.iter().take(5) {
611                let tag_width = (tag.len() as f32).mul_add(6.0, 12.0);
612                canvas.fill_rect(
613                    Rect::new(tx, self.bounds.y + y_offset, tag_width, 18.0),
614                    tag_bg,
615                );
616                canvas.draw_text(
617                    tag,
618                    Point::new(tx + 6.0, self.bounds.y + y_offset + 13.0),
619                    &tag_style,
620                );
621                tx += tag_width + 6.0;
622            }
623        }
624    }
625
626    fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
627        None
628    }
629
630    fn children(&self) -> &[Box<dyn Widget>] {
631        &[]
632    }
633
634    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
635        &mut []
636    }
637
638    fn is_interactive(&self) -> bool {
639        false
640    }
641
642    fn is_focusable(&self) -> bool {
643        false
644    }
645
646    fn accessible_name(&self) -> Option<&str> {
647        self.accessible_name_value.as_deref().or(Some(&self.name))
648    }
649
650    fn accessible_role(&self) -> AccessibleRole {
651        AccessibleRole::Generic
652    }
653
654    fn test_id(&self) -> Option<&str> {
655        self.test_id_value.as_deref()
656    }
657}
658
659// PROBAR-SPEC-009: Brick Architecture - Tests define interface
660impl Brick for ModelCard {
661    fn brick_name(&self) -> &'static str {
662        "ModelCard"
663    }
664
665    fn assertions(&self) -> &[BrickAssertion] {
666        &[BrickAssertion::MaxLatencyMs(16)]
667    }
668
669    fn budget(&self) -> BrickBudget {
670        BrickBudget::uniform(16)
671    }
672
673    fn verify(&self) -> BrickVerification {
674        BrickVerification {
675            passed: self.assertions().to_vec(),
676            failed: vec![],
677            verification_time: Duration::from_micros(10),
678        }
679    }
680
681    fn to_html(&self) -> String {
682        r#"<div class="brick-modelcard"></div>"#.to_string()
683    }
684
685    fn to_css(&self) -> String {
686        ".brick-modelcard { display: block; }".to_string()
687    }
688
689    fn test_id(&self) -> Option<&str> {
690        self.test_id_value.as_deref()
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697
698    // ===== ModelStatus Tests =====
699
700    #[test]
701    fn test_model_status_default() {
702        assert_eq!(ModelStatus::default(), ModelStatus::Draft);
703    }
704
705    #[test]
706    fn test_model_status_color() {
707        let published = ModelStatus::Published;
708        let color = published.color();
709        assert!(color.g > color.r); // Green-ish
710    }
711
712    #[test]
713    fn test_model_status_label() {
714        assert_eq!(ModelStatus::Draft.label(), "Draft");
715        assert_eq!(ModelStatus::Review.label(), "Review");
716        assert_eq!(ModelStatus::Published.label(), "Published");
717        assert_eq!(ModelStatus::Deprecated.label(), "Deprecated");
718        assert_eq!(ModelStatus::Archived.label(), "Archived");
719    }
720
721    // ===== ModelMetric Tests =====
722
723    #[test]
724    fn test_model_metric_new() {
725        let metric = ModelMetric::new("Accuracy", 0.95);
726        assert_eq!(metric.name, "Accuracy");
727        assert_eq!(metric.value, 0.95);
728        assert!(metric.unit.is_none());
729        assert!(metric.higher_is_better);
730    }
731
732    #[test]
733    fn test_model_metric_unit() {
734        let metric = ModelMetric::new("Latency", 45.0).unit("ms");
735        assert_eq!(metric.unit, Some("ms".to_string()));
736    }
737
738    #[test]
739    fn test_model_metric_lower_is_better() {
740        let metric = ModelMetric::new("Loss", 0.05).lower_is_better();
741        assert!(!metric.higher_is_better);
742    }
743
744    #[test]
745    fn test_model_metric_formatted_value_percentage() {
746        let metric = ModelMetric::new("Accuracy", 0.95);
747        assert_eq!(metric.formatted_value(), "95.00%");
748    }
749
750    #[test]
751    fn test_model_metric_formatted_value_with_unit() {
752        let metric = ModelMetric::new("Latency", 45.0).unit("ms");
753        assert_eq!(metric.formatted_value(), "45.00ms");
754    }
755
756    #[test]
757    fn test_model_metric_formatted_value_large() {
758        let metric = ModelMetric::new("Score", 1234.5);
759        assert_eq!(metric.formatted_value(), "1234.50");
760    }
761
762    // ===== ModelCard Construction Tests =====
763
764    #[test]
765    fn test_model_card_new() {
766        let card = ModelCard::new("GPT-4");
767        assert_eq!(card.get_name(), "GPT-4");
768        assert_eq!(card.get_version(), "1.0.0");
769        assert_eq!(card.get_status(), ModelStatus::Draft);
770    }
771
772    #[test]
773    fn test_model_card_default() {
774        let card = ModelCard::default();
775        assert!(card.name.is_empty());
776        assert_eq!(card.version, "1.0.0");
777    }
778
779    #[test]
780    fn test_model_card_builder() {
781        let card = ModelCard::new("ResNet-50")
782            .version("2.1.0")
783            .description("Image classification model")
784            .status(ModelStatus::Published)
785            .framework("PyTorch")
786            .task("classification")
787            .metric(ModelMetric::new("Top-1 Accuracy", 0.761))
788            .metric(ModelMetric::new("Top-5 Accuracy", 0.929))
789            .parameters(25_600_000)
790            .dataset("ImageNet")
791            .author("Deep Learning Team")
792            .tag("vision")
793            .tag("classification")
794            .metadata_entry("license", "Apache-2.0")
795            .width(400.0)
796            .height(300.0)
797            .background(Color::WHITE)
798            .border_color(Color::new(0.8, 0.8, 0.8, 1.0))
799            .corner_radius(12.0)
800            .show_metrics_chart(true)
801            .accessible_name("ResNet-50 model card")
802            .test_id("resnet-card");
803
804        assert_eq!(card.get_name(), "ResNet-50");
805        assert_eq!(card.get_version(), "2.1.0");
806        assert_eq!(card.get_description(), Some("Image classification model"));
807        assert_eq!(card.get_status(), ModelStatus::Published);
808        assert_eq!(card.get_framework(), Some("PyTorch"));
809        assert_eq!(card.get_task(), Some("classification"));
810        assert_eq!(card.get_metrics().len(), 2);
811        assert_eq!(card.get_parameters(), Some(25_600_000));
812        assert_eq!(card.get_dataset(), Some("ImageNet"));
813        assert_eq!(card.get_author(), Some("Deep Learning Team"));
814        assert_eq!(card.get_tags().len(), 2);
815        assert_eq!(card.get_metadata("license"), Some("Apache-2.0"));
816        assert_eq!(Widget::accessible_name(&card), Some("ResNet-50 model card"));
817        assert_eq!(Widget::test_id(&card), Some("resnet-card"));
818    }
819
820    #[test]
821    fn test_model_card_metrics() {
822        let metrics = vec![
823            ModelMetric::new("Accuracy", 0.95),
824            ModelMetric::new("F1", 0.92),
825        ];
826        let card = ModelCard::new("Model").metrics(metrics);
827        assert_eq!(card.get_metrics().len(), 2);
828        assert!(card.has_metrics());
829    }
830
831    #[test]
832    fn test_model_card_tags() {
833        let card = ModelCard::new("Model").tags(["nlp", "transformer", "bert"]);
834        assert_eq!(card.get_tags().len(), 3);
835    }
836
837    // ===== Formatted Parameters Tests =====
838
839    #[test]
840    fn test_formatted_parameters_none() {
841        let card = ModelCard::new("Model");
842        assert!(card.formatted_parameters().is_none());
843    }
844
845    #[test]
846    fn test_formatted_parameters_small() {
847        let card = ModelCard::new("Model").parameters(500);
848        assert_eq!(card.formatted_parameters(), Some("500".to_string()));
849    }
850
851    #[test]
852    fn test_formatted_parameters_thousands() {
853        let card = ModelCard::new("Model").parameters(25_000);
854        assert_eq!(card.formatted_parameters(), Some("25.0K".to_string()));
855    }
856
857    #[test]
858    fn test_formatted_parameters_millions() {
859        let card = ModelCard::new("Model").parameters(125_000_000);
860        assert_eq!(card.formatted_parameters(), Some("125.0M".to_string()));
861    }
862
863    #[test]
864    fn test_formatted_parameters_billions() {
865        let card = ModelCard::new("Model").parameters(175_000_000_000);
866        assert_eq!(card.formatted_parameters(), Some("175.0B".to_string()));
867    }
868
869    // ===== Dimension Tests =====
870
871    #[test]
872    fn test_model_card_width_min() {
873        let card = ModelCard::new("Model").width(100.0);
874        assert_eq!(card.width, Some(200.0));
875    }
876
877    #[test]
878    fn test_model_card_height_min() {
879        let card = ModelCard::new("Model").height(50.0);
880        assert_eq!(card.height, Some(150.0));
881    }
882
883    #[test]
884    fn test_model_card_corner_radius_min() {
885        let card = ModelCard::new("Model").corner_radius(-5.0);
886        assert_eq!(card.corner_radius, 0.0);
887    }
888
889    // ===== Widget Trait Tests =====
890
891    #[test]
892    fn test_model_card_type_id() {
893        let card = ModelCard::new("Model");
894        assert_eq!(Widget::type_id(&card), TypeId::of::<ModelCard>());
895    }
896
897    #[test]
898    fn test_model_card_measure_default() {
899        let card = ModelCard::new("Model");
900        let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
901        assert_eq!(size.width, 320.0);
902        assert_eq!(size.height, 200.0);
903    }
904
905    #[test]
906    fn test_model_card_measure_custom() {
907        let card = ModelCard::new("Model").width(400.0).height(250.0);
908        let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
909        assert_eq!(size.width, 400.0);
910        assert_eq!(size.height, 250.0);
911    }
912
913    #[test]
914    fn test_model_card_layout() {
915        let mut card = ModelCard::new("Model");
916        let bounds = Rect::new(10.0, 20.0, 320.0, 200.0);
917        let result = card.layout(bounds);
918        assert_eq!(result.size, Size::new(320.0, 200.0));
919        assert_eq!(card.bounds, bounds);
920    }
921
922    #[test]
923    fn test_model_card_children() {
924        let card = ModelCard::new("Model");
925        assert!(card.children().is_empty());
926    }
927
928    #[test]
929    fn test_model_card_is_interactive() {
930        let card = ModelCard::new("Model");
931        assert!(!card.is_interactive());
932    }
933
934    #[test]
935    fn test_model_card_is_focusable() {
936        let card = ModelCard::new("Model");
937        assert!(!card.is_focusable());
938    }
939
940    #[test]
941    fn test_model_card_accessible_role() {
942        let card = ModelCard::new("Model");
943        assert_eq!(card.accessible_role(), AccessibleRole::Generic);
944    }
945
946    #[test]
947    fn test_model_card_accessible_name_from_name() {
948        let card = ModelCard::new("GPT-4");
949        assert_eq!(Widget::accessible_name(&card), Some("GPT-4"));
950    }
951
952    #[test]
953    fn test_model_card_accessible_name_explicit() {
954        let card = ModelCard::new("GPT-4").accessible_name("Language model card");
955        assert_eq!(Widget::accessible_name(&card), Some("Language model card"));
956    }
957
958    #[test]
959    fn test_model_card_test_id() {
960        let card = ModelCard::new("Model").test_id("model-card");
961        assert_eq!(Widget::test_id(&card), Some("model-card"));
962    }
963
964    // ===== Has Metrics Tests =====
965
966    #[test]
967    fn test_model_card_has_metrics_false() {
968        let card = ModelCard::new("Model");
969        assert!(!card.has_metrics());
970    }
971
972    #[test]
973    fn test_model_card_has_metrics_true() {
974        let card = ModelCard::new("Model").metric(ModelMetric::new("Acc", 0.9));
975        assert!(card.has_metrics());
976    }
977
978    // =========================================================================
979    // Additional Coverage Tests
980    // =========================================================================
981
982    #[test]
983    fn test_model_status_color_all_variants() {
984        let _ = ModelStatus::Draft.color();
985        let _ = ModelStatus::Review.color();
986        let _ = ModelStatus::Published.color();
987        let _ = ModelStatus::Deprecated.color();
988        let _ = ModelStatus::Archived.color();
989    }
990
991    #[test]
992    fn test_model_card_children_mut() {
993        let mut card = ModelCard::new("Model");
994        assert!(card.children_mut().is_empty());
995    }
996
997    #[test]
998    fn test_model_card_event_returns_none() {
999        let mut card = ModelCard::new("Model");
1000        let result = card.event(&presentar_core::Event::KeyDown {
1001            key: presentar_core::Key::Down,
1002        });
1003        assert!(result.is_none());
1004    }
1005
1006    #[test]
1007    fn test_model_card_test_id_none() {
1008        let card = ModelCard::new("Model");
1009        assert!(Widget::test_id(&card).is_none());
1010    }
1011
1012    #[test]
1013    fn test_model_card_bounds() {
1014        let mut card = ModelCard::new("Model");
1015        card.layout(Rect::new(5.0, 10.0, 320.0, 200.0));
1016        assert_eq!(card.bounds.x, 5.0);
1017        assert_eq!(card.bounds.y, 10.0);
1018    }
1019
1020    #[test]
1021    fn test_model_metric_eq() {
1022        let m1 = ModelMetric::new("Acc", 0.95);
1023        let m2 = ModelMetric::new("Acc", 0.95);
1024        assert_eq!(m1.name, m2.name);
1025        assert_eq!(m1.value, m2.value);
1026    }
1027
1028    #[test]
1029    fn test_model_card_name_setter() {
1030        let card = ModelCard::new("Initial").name("Changed");
1031        assert_eq!(card.get_name(), "Changed");
1032    }
1033
1034    // =========================================================================
1035    // Brick Trait Tests
1036    // =========================================================================
1037
1038    #[test]
1039    fn test_model_card_brick_name() {
1040        let card = ModelCard::new("test");
1041        assert_eq!(card.brick_name(), "ModelCard");
1042    }
1043
1044    #[test]
1045    fn test_model_card_brick_assertions() {
1046        let card = ModelCard::new("test");
1047        let assertions = card.assertions();
1048        assert!(!assertions.is_empty());
1049        assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
1050    }
1051
1052    #[test]
1053    fn test_model_card_brick_budget() {
1054        let card = ModelCard::new("test");
1055        let budget = card.budget();
1056        // Verify budget has reasonable values
1057        assert!(budget.layout_ms > 0);
1058        assert!(budget.paint_ms > 0);
1059    }
1060
1061    #[test]
1062    fn test_model_card_brick_verify() {
1063        let card = ModelCard::new("test");
1064        let verification = card.verify();
1065        assert!(!verification.passed.is_empty());
1066        assert!(verification.failed.is_empty());
1067    }
1068
1069    #[test]
1070    fn test_model_card_brick_to_html() {
1071        let card = ModelCard::new("test");
1072        let html = card.to_html();
1073        assert!(html.contains("brick-modelcard"));
1074    }
1075
1076    #[test]
1077    fn test_model_card_brick_to_css() {
1078        let card = ModelCard::new("test");
1079        let css = card.to_css();
1080        assert!(css.contains(".brick-modelcard"));
1081        assert!(css.contains("display: block"));
1082    }
1083
1084    #[test]
1085    fn test_model_card_brick_test_id() {
1086        let card = ModelCard::new("test").test_id("card-1");
1087        assert_eq!(Brick::test_id(&card), Some("card-1"));
1088    }
1089
1090    #[test]
1091    fn test_model_card_brick_test_id_none() {
1092        let card = ModelCard::new("test");
1093        assert!(Brick::test_id(&card).is_none());
1094    }
1095
1096    // =========================================================================
1097    // ModelStatus Additional Tests
1098    // =========================================================================
1099
1100    #[test]
1101    fn test_model_status_debug() {
1102        let status = ModelStatus::Published;
1103        let debug_str = format!("{:?}", status);
1104        assert!(debug_str.contains("Published"));
1105    }
1106
1107    #[test]
1108    fn test_model_status_eq() {
1109        assert_eq!(ModelStatus::Draft, ModelStatus::Draft);
1110        assert_ne!(ModelStatus::Draft, ModelStatus::Published);
1111    }
1112
1113    #[test]
1114    fn test_model_status_clone() {
1115        let status = ModelStatus::Review;
1116        let cloned = status;
1117        assert_eq!(cloned, ModelStatus::Review);
1118    }
1119
1120    #[test]
1121    fn test_model_status_serde() {
1122        let status = ModelStatus::Deprecated;
1123        let serialized = serde_json::to_string(&status).unwrap();
1124        let deserialized: ModelStatus = serde_json::from_str(&serialized).unwrap();
1125        assert_eq!(deserialized, ModelStatus::Deprecated);
1126    }
1127
1128    #[test]
1129    fn test_model_status_color_all_variants_detailed() {
1130        // Draft is gray
1131        let draft_color = ModelStatus::Draft.color();
1132        assert!((draft_color.r - 0.6).abs() < 0.01);
1133
1134        // Review is yellow/orange
1135        let review_color = ModelStatus::Review.color();
1136        assert!(review_color.r > 0.8);
1137        assert!(review_color.g > 0.6);
1138
1139        // Published is green
1140        let published_color = ModelStatus::Published.color();
1141        assert!(published_color.g > published_color.r);
1142
1143        // Deprecated is orange
1144        let deprecated_color = ModelStatus::Deprecated.color();
1145        assert!(deprecated_color.r > 0.8);
1146
1147        // Archived is gray
1148        let archived_color = ModelStatus::Archived.color();
1149        assert!((archived_color.r - 0.5).abs() < 0.01);
1150    }
1151
1152    // =========================================================================
1153    // ModelMetric Additional Tests
1154    // =========================================================================
1155
1156    #[test]
1157    fn test_model_metric_debug() {
1158        let metric = ModelMetric::new("Accuracy", 0.95);
1159        let debug_str = format!("{:?}", metric);
1160        assert!(debug_str.contains("Accuracy"));
1161        assert!(debug_str.contains("0.95"));
1162    }
1163
1164    #[test]
1165    fn test_model_metric_clone() {
1166        let metric = ModelMetric::new("F1", 0.88).unit("%").lower_is_better();
1167        let cloned = metric.clone();
1168        assert_eq!(cloned.name, "F1");
1169        assert_eq!(cloned.value, 0.88);
1170        assert_eq!(cloned.unit, Some("%".to_string()));
1171        assert!(!cloned.higher_is_better);
1172    }
1173
1174    #[test]
1175    fn test_model_metric_serde() {
1176        let metric = ModelMetric::new("Loss", 0.05).lower_is_better();
1177        let serialized = serde_json::to_string(&metric).unwrap();
1178        let deserialized: ModelMetric = serde_json::from_str(&serialized).unwrap();
1179        assert_eq!(deserialized.name, "Loss");
1180        assert_eq!(deserialized.value, 0.05);
1181        assert!(!deserialized.higher_is_better);
1182    }
1183
1184    #[test]
1185    fn test_model_metric_formatted_value_negative() {
1186        let metric = ModelMetric::new("Correlation", -0.5);
1187        let formatted = metric.formatted_value();
1188        assert!(formatted.contains("-50.00%"));
1189    }
1190
1191    #[test]
1192    fn test_model_metric_formatted_value_zero() {
1193        let metric = ModelMetric::new("Bias", 0.0);
1194        assert_eq!(metric.formatted_value(), "0.00%");
1195    }
1196
1197    #[test]
1198    fn test_model_metric_formatted_value_exactly_one() {
1199        let metric = ModelMetric::new("Perfect", 1.0);
1200        assert_eq!(metric.formatted_value(), "1.00");
1201    }
1202
1203    // =========================================================================
1204    // ModelCard Additional Tests
1205    // =========================================================================
1206
1207    #[test]
1208    fn test_model_card_debug() {
1209        let card = ModelCard::new("GPT-4");
1210        let debug_str = format!("{:?}", card);
1211        assert!(debug_str.contains("GPT-4"));
1212    }
1213
1214    #[test]
1215    fn test_model_card_clone() {
1216        let card = ModelCard::new("BERT")
1217            .version("2.0.0")
1218            .status(ModelStatus::Published)
1219            .framework("PyTorch");
1220        let cloned = card.clone();
1221        assert_eq!(cloned.get_name(), "BERT");
1222        assert_eq!(cloned.get_version(), "2.0.0");
1223        assert_eq!(cloned.get_status(), ModelStatus::Published);
1224        assert_eq!(cloned.get_framework(), Some("PyTorch"));
1225    }
1226
1227    #[test]
1228    fn test_model_card_serde() {
1229        let card = ModelCard::new("ResNet")
1230            .version("1.0.0")
1231            .status(ModelStatus::Draft);
1232        let serialized = serde_json::to_string(&card).unwrap();
1233        let deserialized: ModelCard = serde_json::from_str(&serialized).unwrap();
1234        assert_eq!(deserialized.get_name(), "ResNet");
1235        assert_eq!(deserialized.get_version(), "1.0.0");
1236        assert_eq!(deserialized.get_status(), ModelStatus::Draft);
1237    }
1238
1239    // =========================================================================
1240    // Getter Tests (ensure all getters are covered)
1241    // =========================================================================
1242
1243    #[test]
1244    fn test_model_card_getters_none() {
1245        let card = ModelCard::new("test");
1246        assert!(card.get_description().is_none());
1247        assert!(card.get_framework().is_none());
1248        assert!(card.get_task().is_none());
1249        assert!(card.get_parameters().is_none());
1250        assert!(card.get_dataset().is_none());
1251        assert!(card.get_author().is_none());
1252        assert!(card.get_metadata("nonexistent").is_none());
1253    }
1254
1255    #[test]
1256    fn test_model_card_getters_some() {
1257        let card = ModelCard::new("test")
1258            .description("desc")
1259            .framework("TensorFlow")
1260            .task("classification")
1261            .parameters(1_000_000)
1262            .dataset("CIFAR-10")
1263            .author("ML Team")
1264            .metadata_entry("license", "Apache-2.0");
1265
1266        assert_eq!(card.get_description(), Some("desc"));
1267        assert_eq!(card.get_framework(), Some("TensorFlow"));
1268        assert_eq!(card.get_task(), Some("classification"));
1269        assert_eq!(card.get_parameters(), Some(1_000_000));
1270        assert_eq!(card.get_dataset(), Some("CIFAR-10"));
1271        assert_eq!(card.get_author(), Some("ML Team"));
1272        assert_eq!(card.get_metadata("license"), Some("Apache-2.0"));
1273    }
1274
1275    // =========================================================================
1276    // Widget Trait Additional Tests
1277    // =========================================================================
1278
1279    #[test]
1280    fn test_model_card_measure_with_tight_constraints() {
1281        let card = ModelCard::new("test").width(400.0).height(300.0);
1282        let size = card.measure(Constraints::tight(Size::new(200.0, 150.0)));
1283        assert_eq!(size.width, 200.0);
1284        assert_eq!(size.height, 150.0);
1285    }
1286
1287    // =========================================================================
1288    // Edge Case Tests
1289    // =========================================================================
1290
1291    #[test]
1292    fn test_model_card_empty_metrics() {
1293        let card = ModelCard::new("test").metrics(vec![]);
1294        assert!(!card.has_metrics());
1295        assert!(card.get_metrics().is_empty());
1296    }
1297
1298    #[test]
1299    fn test_model_card_many_metrics() {
1300        let metrics: Vec<ModelMetric> = (0..5)
1301            .map(|i| ModelMetric::new(format!("metric_{i}"), i as f64 * 0.1))
1302            .collect();
1303        let card = ModelCard::new("test").metrics(metrics);
1304        assert!(card.has_metrics());
1305        assert_eq!(card.get_metrics().len(), 5);
1306    }
1307
1308    #[test]
1309    fn test_model_card_empty_tags() {
1310        let tags: [&str; 0] = [];
1311        let card = ModelCard::new("test").tags(tags);
1312        assert!(card.get_tags().is_empty());
1313    }
1314
1315    #[test]
1316    fn test_model_card_show_metrics_chart_false() {
1317        let card = ModelCard::new("test")
1318            .metric(ModelMetric::new("acc", 0.95))
1319            .show_metrics_chart(false);
1320        assert!(card.has_metrics());
1321        // show_metrics_chart only affects paint, not has_metrics
1322    }
1323
1324    #[test]
1325    fn test_model_card_default_colors() {
1326        let card = ModelCard::default();
1327        assert_eq!(card.background, Color::WHITE);
1328    }
1329
1330    #[test]
1331    fn test_model_card_default_values() {
1332        let card = ModelCard::default();
1333        assert!(card.name.is_empty());
1334        assert_eq!(card.version, "1.0.0");
1335        assert_eq!(card.status, ModelStatus::Draft);
1336        assert!(card.show_metrics_chart);
1337        assert_eq!(card.corner_radius, 8.0);
1338    }
1339
1340    // =========================================================================
1341    // Formatted Parameters Edge Cases
1342    // =========================================================================
1343
1344    #[test]
1345    fn test_formatted_parameters_edge_cases() {
1346        // Exactly 1000 parameters
1347        let card = ModelCard::new("test").parameters(1000);
1348        assert_eq!(card.formatted_parameters(), Some("1.0K".to_string()));
1349
1350        // Exactly 1 million
1351        let card = ModelCard::new("test").parameters(1_000_000);
1352        assert_eq!(card.formatted_parameters(), Some("1.0M".to_string()));
1353
1354        // Exactly 1 billion
1355        let card = ModelCard::new("test").parameters(1_000_000_000);
1356        assert_eq!(card.formatted_parameters(), Some("1.0B".to_string()));
1357    }
1358}