Skip to main content

cloudflare_dns/ui/
status.rs

1#![allow(dead_code)]
2
3/// Status message types and rendering.
4use crate::ui::state::AppView;
5
6/// Represents the type of status message
7#[derive(Debug, Clone, PartialEq)]
8pub enum StatusType {
9    /// Transient messages that should display briefly
10    Transient,
11    /// Persistent contextual help based on current view
12    Contextual,
13}
14
15/// Context for form field help messages
16#[derive(Debug, Clone)]
17pub enum FormFieldContext {
18    Type,
19    Name,
20    Content,
21    Ttl,
22    Proxied,
23    Submit,
24}
25
26/// All possible status message variants
27#[derive(Debug, Clone)]
28pub enum StatusMessage {
29    /// Initial loading state
30    Initializing,
31    /// Loading DNS records
32    LoadingRecords,
33    /// Error message
34    Error(String),
35    /// Operation results (created, updated, deleted, etc.)
36    OperationResult(String),
37    /// View-specific contextual help
38    ViewHelp(ViewHelpContext),
39    /// Form field help
40    FormFieldHelp {
41        context: FormFieldContext,
42        form_type: String,
43        form_proxied: String,
44        is_editing: bool,
45    },
46    /// Record list navigation help
47    RecordListHelp {
48        position: usize,
49        total: usize,
50        record_name: String,
51    },
52    /// Empty list help
53    EmptyListHelp,
54}
55
56/// View-specific help contexts
57#[derive(Debug, Clone)]
58pub enum ViewHelpContext {
59    DeleteConfirmation,
60    IpSelector,
61}
62
63impl StatusMessage {
64    /// Determine if this status should be transient (auto-clearing)
65    pub fn status_type(&self) -> StatusType {
66        match self {
67            StatusMessage::Error(_)
68            | StatusMessage::OperationResult(_)
69            | StatusMessage::Initializing
70            | StatusMessage::LoadingRecords => StatusType::Transient,
71            _ => StatusType::Contextual,
72        }
73    }
74
75    /// Check if a status string represents a transient message
76    pub fn is_transient(status_str: &str) -> bool {
77        status_str.starts_with("Error:")
78            || status_str.starts_with("Refreshing...")
79            || status_str.starts_with("Created")
80            || status_str.starts_with("Updated")
81            || status_str.starts_with("Deleted")
82            || status_str.starts_with("Failed")
83            || status_str.starts_with("Selected")
84            || status_str.starts_with("Cancelled")
85    }
86
87    /// Render the status message to a display string
88    pub fn render(&self) -> String {
89        match self {
90            StatusMessage::Initializing => "Initializing...".to_string(),
91            StatusMessage::LoadingRecords => "Loading DNS records...".to_string(),
92            StatusMessage::Error(msg) => format!("Error: {}", msg),
93            StatusMessage::OperationResult(msg) => msg.clone(),
94            StatusMessage::ViewHelp(ViewHelpContext::DeleteConfirmation) => {
95                "Enter: confirm deletion | Esc: cancel".to_string()
96            }
97            StatusMessage::ViewHelp(ViewHelpContext::IpSelector) => {
98                "↑↓: navigate | Enter: select IP | Esc: back to form".to_string()
99            }
100            StatusMessage::FormFieldHelp {
101                context,
102                form_type,
103                form_proxied,
104                is_editing,
105            } => match context {
106                FormFieldContext::Type => {
107                    format!(
108                        "Field 1/6 — Type: {} | Press Space to cycle types",
109                        form_type
110                    )
111                }
112                FormFieldContext::Name => "Field 2/6 — Name: e.g. nginx".to_string(),
113                FormFieldContext::Content => {
114                    "Field 3/6 — IP Address | Press Space to use an existing address | Type: enter IP"
115                        .to_string()
116                }
117                FormFieldContext::Ttl => "Field 4/6 — TTL: seconds (1 = auto)".to_string(),
118                FormFieldContext::Proxied => {
119                    let proxied_status = if form_proxied == "true" {
120                        "Orange cloud ON"
121                    } else {
122                        "Grey cloud OFF"
123                    };
124                    format!(
125                        "Field 5/6 — Proxied: {} | Press Space to toggle",
126                        proxied_status
127                    )
128                }
129                FormFieldContext::Submit => {
130                    let action = if *is_editing { "Save" } else { "Create" };
131                    format!("Field 6/6 — Press Enter to {} record", action)
132                }
133            },
134            StatusMessage::RecordListHelp {
135                position,
136                total,
137                record_name,
138            } => {
139                format!(
140                    "{} of {} — {} | e: edit | d: delete | r: refresh | c: create | q: quit",
141                    position, total, record_name
142                )
143            }
144            StatusMessage::EmptyListHelp => {
145                "No records | c: create your first DNS record | q: quit".to_string()
146            }
147        }
148    }
149}
150
151/// Generate the appropriate contextual status message based on current state
152#[allow(clippy::too_many_arguments)]
153pub fn generate_contextual_status(
154    view: &AppView,
155    form_focus: usize,
156    form_type: &str,
157    form_proxied: &str,
158    is_editing: bool,
159    record_count: usize,
160    selected_record_idx: usize,
161    selected_record_name: Option<&str>,
162) -> StatusMessage {
163    match view {
164        AppView::Delete => StatusMessage::ViewHelp(ViewHelpContext::DeleteConfirmation),
165        AppView::IpSelect => StatusMessage::ViewHelp(ViewHelpContext::IpSelector),
166        AppView::Create | AppView::Edit => {
167            let context = match form_focus {
168                0 => FormFieldContext::Type,
169                1 => FormFieldContext::Name,
170                2 => FormFieldContext::Content,
171                3 => FormFieldContext::Ttl,
172                4 => FormFieldContext::Proxied,
173                5 => FormFieldContext::Submit,
174                _ => FormFieldContext::Name, // fallback
175            };
176            StatusMessage::FormFieldHelp {
177                context,
178                form_type: form_type.to_string(),
179                form_proxied: form_proxied.to_string(),
180                is_editing,
181            }
182        }
183        AppView::List => {
184            if record_count > 0 && selected_record_idx < record_count {
185                StatusMessage::RecordListHelp {
186                    position: selected_record_idx + 1,
187                    total: record_count,
188                    record_name: selected_record_name.unwrap_or("Unknown").to_string(),
189                }
190            } else {
191                StatusMessage::EmptyListHelp
192            }
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_status_type_transient_error() {
203        let status = StatusMessage::Error("test error".to_string());
204        assert_eq!(status.status_type(), StatusType::Transient);
205    }
206
207    #[test]
208    fn test_status_type_transient_operation_result() {
209        let status = StatusMessage::OperationResult("Created A for example".to_string());
210        assert_eq!(status.status_type(), StatusType::Transient);
211    }
212
213    #[test]
214    fn test_status_type_transient_initializing() {
215        let status = StatusMessage::Initializing;
216        assert_eq!(status.status_type(), StatusType::Transient);
217    }
218
219    #[test]
220    fn test_status_type_transient_loading_records() {
221        let status = StatusMessage::LoadingRecords;
222        assert_eq!(status.status_type(), StatusType::Transient);
223    }
224
225    #[test]
226    fn test_status_type_contextual_view_help() {
227        let status = StatusMessage::ViewHelp(ViewHelpContext::DeleteConfirmation);
228        assert_eq!(status.status_type(), StatusType::Contextual);
229    }
230
231    #[test]
232    fn test_status_type_contextual_form_field_help() {
233        let status = StatusMessage::FormFieldHelp {
234            context: FormFieldContext::Name,
235            form_type: "A".to_string(),
236            form_proxied: "false".to_string(),
237            is_editing: false,
238        };
239        assert_eq!(status.status_type(), StatusType::Contextual);
240    }
241
242    #[test]
243    fn test_status_type_contextual_record_list_help() {
244        let status = StatusMessage::RecordListHelp {
245            position: 1,
246            total: 5,
247            record_name: "example.com".to_string(),
248        };
249        assert_eq!(status.status_type(), StatusType::Contextual);
250    }
251
252    #[test]
253    fn test_status_type_contextual_empty_list_help() {
254        let status = StatusMessage::EmptyListHelp;
255        assert_eq!(status.status_type(), StatusType::Contextual);
256    }
257
258    #[test]
259    fn test_is_transient_error() {
260        assert!(StatusMessage::is_transient("Error: something went wrong"));
261    }
262
263    #[test]
264    fn test_is_transient_created() {
265        assert!(StatusMessage::is_transient("Created A for example"));
266    }
267
268    #[test]
269    fn test_is_transient_updated() {
270        assert!(StatusMessage::is_transient("Updated A for example"));
271    }
272
273    #[test]
274    fn test_is_transient_deleted() {
275        assert!(StatusMessage::is_transient("Deleted A for example"));
276    }
277
278    #[test]
279    fn test_is_transient_failed() {
280        assert!(StatusMessage::is_transient("Failed: API error"));
281    }
282
283    #[test]
284    fn test_is_transient_selected() {
285        assert!(StatusMessage::is_transient("Selected IP address"));
286    }
287
288    #[test]
289    fn test_is_transient_cancelled() {
290        assert!(StatusMessage::is_transient("Cancelled operation"));
291    }
292
293    #[test]
294    fn test_is_not_transient_contextual() {
295        assert!(!StatusMessage::is_transient(
296            "Enter: confirm deletion | Esc: cancel"
297        ));
298    }
299
300    #[test]
301    fn test_is_not_transient_record_list() {
302        assert!(!StatusMessage::is_transient(
303            "1 of 5 — example.com | E: edit | D: delete | R: refresh | C: create | Q: quit"
304        ));
305    }
306
307    #[test]
308    fn test_render_initializing() {
309        let status = StatusMessage::Initializing;
310        assert_eq!(status.render(), "Initializing...");
311    }
312
313    #[test]
314    fn test_render_loading_records() {
315        let status = StatusMessage::LoadingRecords;
316        assert_eq!(status.render(), "Loading DNS records...");
317    }
318
319    #[test]
320    fn test_render_error() {
321        let status = StatusMessage::Error("connection failed".to_string());
322        assert_eq!(status.render(), "Error: connection failed");
323    }
324
325    #[test]
326    fn test_render_operation_result() {
327        let status = StatusMessage::OperationResult("Created A record".to_string());
328        assert_eq!(status.render(), "Created A record");
329    }
330
331    #[test]
332    fn test_render_delete_confirmation_help() {
333        let status = StatusMessage::ViewHelp(ViewHelpContext::DeleteConfirmation);
334        assert_eq!(status.render(), "Enter: confirm deletion | Esc: cancel");
335    }
336
337    #[test]
338    fn test_render_ip_selector_help() {
339        let status = StatusMessage::ViewHelp(ViewHelpContext::IpSelector);
340        assert_eq!(
341            status.render(),
342            "↑↓: navigate | Enter: select IP | Esc: back to form"
343        );
344    }
345
346    #[test]
347    fn test_render_form_field_type() {
348        let status = StatusMessage::FormFieldHelp {
349            context: FormFieldContext::Type,
350            form_type: "A".to_string(),
351            form_proxied: "false".to_string(),
352            is_editing: false,
353        };
354        assert_eq!(
355            status.render(),
356            "Field 1/6 — Type: A | Press Space to cycle types"
357        );
358    }
359
360    #[test]
361    fn test_render_form_field_name() {
362        let status = StatusMessage::FormFieldHelp {
363            context: FormFieldContext::Name,
364            form_type: "A".to_string(),
365            form_proxied: "false".to_string(),
366            is_editing: false,
367        };
368        assert_eq!(status.render(), "Field 2/6 — Name: e.g. nginx");
369    }
370
371    #[test]
372    fn test_render_form_field_content() {
373        let status = StatusMessage::FormFieldHelp {
374            context: FormFieldContext::Content,
375            form_type: "A".to_string(),
376            form_proxied: "false".to_string(),
377            is_editing: false,
378        };
379        assert_eq!(
380            status.render(),
381            "Field 3/6 — IP Address | Press Space to use an existing address | Type: enter IP"
382        );
383    }
384
385    #[test]
386    fn test_render_form_field_ttl() {
387        let status = StatusMessage::FormFieldHelp {
388            context: FormFieldContext::Ttl,
389            form_type: "A".to_string(),
390            form_proxied: "false".to_string(),
391            is_editing: false,
392        };
393        assert_eq!(status.render(), "Field 4/6 — TTL: seconds (1 = auto)");
394    }
395
396    #[test]
397    fn test_render_form_field_proxied_true() {
398        let status = StatusMessage::FormFieldHelp {
399            context: FormFieldContext::Proxied,
400            form_type: "A".to_string(),
401            form_proxied: "true".to_string(),
402            is_editing: false,
403        };
404        assert_eq!(
405            status.render(),
406            "Field 5/6 — Proxied: Orange cloud ON | Press Space to toggle"
407        );
408    }
409
410    #[test]
411    fn test_render_form_field_proxied_false() {
412        let status = StatusMessage::FormFieldHelp {
413            context: FormFieldContext::Proxied,
414            form_type: "A".to_string(),
415            form_proxied: "false".to_string(),
416            is_editing: false,
417        };
418        assert_eq!(
419            status.render(),
420            "Field 5/6 — Proxied: Grey cloud OFF | Press Space to toggle"
421        );
422    }
423
424    #[test]
425    fn test_render_form_field_submit_create() {
426        let status = StatusMessage::FormFieldHelp {
427            context: FormFieldContext::Submit,
428            form_type: "A".to_string(),
429            form_proxied: "false".to_string(),
430            is_editing: false,
431        };
432        assert_eq!(status.render(), "Field 6/6 — Press Enter to Create record");
433    }
434
435    #[test]
436    fn test_render_form_field_submit_edit() {
437        let status = StatusMessage::FormFieldHelp {
438            context: FormFieldContext::Submit,
439            form_type: "A".to_string(),
440            form_proxied: "false".to_string(),
441            is_editing: true,
442        };
443        assert_eq!(status.render(), "Field 6/6 — Press Enter to Save record");
444    }
445
446    #[test]
447    fn test_render_record_list_help() {
448        let status = StatusMessage::RecordListHelp {
449            position: 3,
450            total: 10,
451            record_name: "test.example.com".to_string(),
452        };
453        assert_eq!(
454            status.render(),
455            "3 of 10 — test.example.com | e: edit | d: delete | r: refresh | c: create | q: quit"
456        );
457    }
458
459    #[test]
460    fn test_render_empty_list_help() {
461        let status = StatusMessage::EmptyListHelp;
462        assert_eq!(
463            status.render(),
464            "No records | c: create your first DNS record | q: quit"
465        );
466    }
467
468    #[test]
469    fn test_generate_contextual_status_delete_view() {
470        let result = generate_contextual_status(
471            &AppView::Delete,
472            0,
473            "A",
474            "false",
475            false,
476            5,
477            0,
478            Some("example.com"),
479        );
480        assert!(matches!(
481            result,
482            StatusMessage::ViewHelp(ViewHelpContext::DeleteConfirmation)
483        ));
484    }
485
486    #[test]
487    fn test_generate_contextual_status_ip_selector_view() {
488        let result = generate_contextual_status(
489            &AppView::IpSelect,
490            0,
491            "A",
492            "false",
493            false,
494            5,
495            0,
496            Some("example.com"),
497        );
498        assert!(matches!(
499            result,
500            StatusMessage::ViewHelp(ViewHelpContext::IpSelector)
501        ));
502    }
503
504    #[test]
505    fn test_generate_contextual_status_create_view_first_field() {
506        let result = generate_contextual_status(
507            &AppView::Create,
508            0,
509            "A",
510            "false",
511            false,
512            5,
513            0,
514            Some("example.com"),
515        );
516        match result {
517            StatusMessage::FormFieldHelp {
518                context: FormFieldContext::Type,
519                form_type,
520                ..
521            } => {
522                assert_eq!(form_type, "A");
523            }
524            _ => panic!("Expected FormFieldHelp with Type context"),
525        }
526    }
527
528    #[test]
529    fn test_generate_contextual_status_create_view_name_field() {
530        let result = generate_contextual_status(
531            &AppView::Create,
532            1,
533            "AAAA",
534            "true",
535            false,
536            5,
537            0,
538            Some("example.com"),
539        );
540        match result {
541            StatusMessage::FormFieldHelp {
542                context: FormFieldContext::Name,
543                form_type,
544                form_proxied,
545                ..
546            } => {
547                assert_eq!(form_type, "AAAA");
548                assert_eq!(form_proxied, "true");
549            }
550            _ => panic!("Expected FormFieldHelp with Name context"),
551        }
552    }
553
554    #[test]
555    fn test_generate_contextual_status_edit_view_submit_field() {
556        let result = generate_contextual_status(
557            &AppView::Edit,
558            5,
559            "CNAME",
560            "false",
561            true,
562            5,
563            0,
564            Some("example.com"),
565        );
566        match result {
567            StatusMessage::FormFieldHelp {
568                context: FormFieldContext::Submit,
569                is_editing,
570                ..
571            } => {
572                assert!(is_editing);
573            }
574            _ => panic!("Expected FormFieldHelp with Submit context"),
575        }
576    }
577
578    #[test]
579    fn test_generate_contextual_status_list_view_with_records() {
580        let result = generate_contextual_status(
581            &AppView::List,
582            0,
583            "A",
584            "false",
585            false,
586            5,
587            2,
588            Some("test.example.com"),
589        );
590        match result {
591            StatusMessage::RecordListHelp {
592                position,
593                total,
594                record_name,
595            } => {
596                assert_eq!(position, 3); // idx + 1
597                assert_eq!(total, 5);
598                assert_eq!(record_name, "test.example.com");
599            }
600            _ => panic!("Expected RecordListHelp"),
601        }
602    }
603
604    #[test]
605    fn test_generate_contextual_status_list_view_empty() {
606        let result = generate_contextual_status(&AppView::List, 0, "A", "false", false, 0, 0, None);
607        assert!(matches!(result, StatusMessage::EmptyListHelp));
608    }
609
610    #[test]
611    fn test_generate_contextual_status_list_view_invalid_selection() {
612        // Selection index beyond record count
613        let result = generate_contextual_status(
614            &AppView::List,
615            0,
616            "A",
617            "false",
618            false,
619            3,
620            5, // idx >= record_count
621            Some("example.com"),
622        );
623        assert!(matches!(result, StatusMessage::EmptyListHelp));
624    }
625
626    #[test]
627    fn test_generate_contextual_status_list_view_unknown_record_name() {
628        let result = generate_contextual_status(
629            &AppView::List,
630            0,
631            "A",
632            "false",
633            false,
634            1,
635            0,
636            None, // No record name
637        );
638        match result {
639            StatusMessage::RecordListHelp { record_name, .. } => {
640                assert_eq!(record_name, "Unknown");
641            }
642            _ => panic!("Expected RecordListHelp"),
643        }
644    }
645
646    #[test]
647    fn test_generate_contextual_status_form_fallback() {
648        // form_focus beyond 0-5 range should fallback to Name context
649        let result = generate_contextual_status(
650            &AppView::Create,
651            10, // out of range
652            "A",
653            "false",
654            false,
655            5,
656            0,
657            Some("example.com"),
658        );
659        match result {
660            StatusMessage::FormFieldHelp {
661                context: FormFieldContext::Name,
662                ..
663            } => {
664                // Expected fallback behavior
665            }
666            _ => panic!("Expected FormFieldHelp with Name context as fallback"),
667        }
668    }
669}