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!(status.render(), "Field 1/6 — Type: A | Press Space to cycle types");
355    }
356
357    #[test]
358    fn test_render_form_field_name() {
359        let status = StatusMessage::FormFieldHelp {
360            context: FormFieldContext::Name,
361            form_type: "A".to_string(),
362            form_proxied: "false".to_string(),
363            is_editing: false,
364        };
365        assert_eq!(status.render(), "Field 2/6 — Name: e.g. nginx");
366    }
367
368    #[test]
369    fn test_render_form_field_content() {
370        let status = StatusMessage::FormFieldHelp {
371            context: FormFieldContext::Content,
372            form_type: "A".to_string(),
373            form_proxied: "false".to_string(),
374            is_editing: false,
375        };
376        assert_eq!(
377            status.render(),
378            "Field 3/6 — IP Address | Press Space to use an existing address | Type: enter IP"
379        );
380    }
381
382    #[test]
383    fn test_render_form_field_ttl() {
384        let status = StatusMessage::FormFieldHelp {
385            context: FormFieldContext::Ttl,
386            form_type: "A".to_string(),
387            form_proxied: "false".to_string(),
388            is_editing: false,
389        };
390        assert_eq!(status.render(), "Field 4/6 — TTL: seconds (1 = auto)");
391    }
392
393    #[test]
394    fn test_render_form_field_proxied_true() {
395        let status = StatusMessage::FormFieldHelp {
396            context: FormFieldContext::Proxied,
397            form_type: "A".to_string(),
398            form_proxied: "true".to_string(),
399            is_editing: false,
400        };
401        assert_eq!(status.render(), "Field 5/6 — Proxied: Orange cloud ON | Press Space to toggle");
402    }
403
404    #[test]
405    fn test_render_form_field_proxied_false() {
406        let status = StatusMessage::FormFieldHelp {
407            context: FormFieldContext::Proxied,
408            form_type: "A".to_string(),
409            form_proxied: "false".to_string(),
410            is_editing: false,
411        };
412        assert_eq!(status.render(), "Field 5/6 — Proxied: Grey cloud OFF | Press Space to toggle");
413    }
414
415    #[test]
416    fn test_render_form_field_submit_create() {
417        let status = StatusMessage::FormFieldHelp {
418            context: FormFieldContext::Submit,
419            form_type: "A".to_string(),
420            form_proxied: "false".to_string(),
421            is_editing: false,
422        };
423        assert_eq!(status.render(), "Field 6/6 — Press Enter to Create record");
424    }
425
426    #[test]
427    fn test_render_form_field_submit_edit() {
428        let status = StatusMessage::FormFieldHelp {
429            context: FormFieldContext::Submit,
430            form_type: "A".to_string(),
431            form_proxied: "false".to_string(),
432            is_editing: true,
433        };
434        assert_eq!(status.render(), "Field 6/6 — Press Enter to Save record");
435    }
436
437    #[test]
438    fn test_render_record_list_help() {
439        let status = StatusMessage::RecordListHelp {
440            position: 3,
441            total: 10,
442            record_name: "test.example.com".to_string(),
443        };
444        assert_eq!(
445            status.render(),
446            "3 of 10 — test.example.com | e: edit | d: delete | r: refresh | c: create | q: quit"
447        );
448    }
449
450    #[test]
451    fn test_render_empty_list_help() {
452        let status = StatusMessage::EmptyListHelp;
453        assert_eq!(
454            status.render(),
455            "No records | c: create your first DNS record | q: quit"
456        );
457    }
458
459    #[test]
460    fn test_generate_contextual_status_delete_view() {
461        let result = generate_contextual_status(
462            &AppView::Delete,
463            0,
464            "A",
465            "false",
466            false,
467            5,
468            0,
469            Some("example.com"),
470        );
471        assert!(matches!(
472            result,
473            StatusMessage::ViewHelp(ViewHelpContext::DeleteConfirmation)
474        ));
475    }
476
477    #[test]
478    fn test_generate_contextual_status_ip_selector_view() {
479        let result = generate_contextual_status(
480            &AppView::IpSelect,
481            0,
482            "A",
483            "false",
484            false,
485            5,
486            0,
487            Some("example.com"),
488        );
489        assert!(matches!(
490            result,
491            StatusMessage::ViewHelp(ViewHelpContext::IpSelector)
492        ));
493    }
494
495    #[test]
496    fn test_generate_contextual_status_create_view_first_field() {
497        let result = generate_contextual_status(
498            &AppView::Create,
499            0,
500            "A",
501            "false",
502            false,
503            5,
504            0,
505            Some("example.com"),
506        );
507        match result {
508            StatusMessage::FormFieldHelp {
509                context: FormFieldContext::Type,
510                form_type,
511                ..
512            } => {
513                assert_eq!(form_type, "A");
514            }
515            _ => panic!("Expected FormFieldHelp with Type context"),
516        }
517    }
518
519    #[test]
520    fn test_generate_contextual_status_create_view_name_field() {
521        let result = generate_contextual_status(
522            &AppView::Create,
523            1,
524            "AAAA",
525            "true",
526            false,
527            5,
528            0,
529            Some("example.com"),
530        );
531        match result {
532            StatusMessage::FormFieldHelp {
533                context: FormFieldContext::Name,
534                form_type,
535                form_proxied,
536                ..
537            } => {
538                assert_eq!(form_type, "AAAA");
539                assert_eq!(form_proxied, "true");
540            }
541            _ => panic!("Expected FormFieldHelp with Name context"),
542        }
543    }
544
545    #[test]
546    fn test_generate_contextual_status_edit_view_submit_field() {
547        let result = generate_contextual_status(
548            &AppView::Edit,
549            5,
550            "CNAME",
551            "false",
552            true,
553            5,
554            0,
555            Some("example.com"),
556        );
557        match result {
558            StatusMessage::FormFieldHelp {
559                context: FormFieldContext::Submit,
560                is_editing,
561                ..
562            } => {
563                assert!(is_editing);
564            }
565            _ => panic!("Expected FormFieldHelp with Submit context"),
566        }
567    }
568
569    #[test]
570    fn test_generate_contextual_status_list_view_with_records() {
571        let result = generate_contextual_status(
572            &AppView::List,
573            0,
574            "A",
575            "false",
576            false,
577            5,
578            2,
579            Some("test.example.com"),
580        );
581        match result {
582            StatusMessage::RecordListHelp {
583                position,
584                total,
585                record_name,
586            } => {
587                assert_eq!(position, 3); // idx + 1
588                assert_eq!(total, 5);
589                assert_eq!(record_name, "test.example.com");
590            }
591            _ => panic!("Expected RecordListHelp"),
592        }
593    }
594
595    #[test]
596    fn test_generate_contextual_status_list_view_empty() {
597        let result = generate_contextual_status(&AppView::List, 0, "A", "false", false, 0, 0, None);
598        assert!(matches!(result, StatusMessage::EmptyListHelp));
599    }
600
601    #[test]
602    fn test_generate_contextual_status_list_view_invalid_selection() {
603        // Selection index beyond record count
604        let result = generate_contextual_status(
605            &AppView::List,
606            0,
607            "A",
608            "false",
609            false,
610            3,
611            5, // idx >= record_count
612            Some("example.com"),
613        );
614        assert!(matches!(result, StatusMessage::EmptyListHelp));
615    }
616
617    #[test]
618    fn test_generate_contextual_status_list_view_unknown_record_name() {
619        let result = generate_contextual_status(
620            &AppView::List,
621            0,
622            "A",
623            "false",
624            false,
625            1,
626            0,
627            None, // No record name
628        );
629        match result {
630            StatusMessage::RecordListHelp { record_name, .. } => {
631                assert_eq!(record_name, "Unknown");
632            }
633            _ => panic!("Expected RecordListHelp"),
634        }
635    }
636
637    #[test]
638    fn test_generate_contextual_status_form_fallback() {
639        // form_focus beyond 0-5 range should fallback to Name context
640        let result = generate_contextual_status(
641            &AppView::Create,
642            10, // out of range
643            "A",
644            "false",
645            false,
646            5,
647            0,
648            Some("example.com"),
649        );
650        match result {
651            StatusMessage::FormFieldHelp {
652                context: FormFieldContext::Name,
653                ..
654            } => {
655                // Expected fallback behavior
656            }
657            _ => panic!("Expected FormFieldHelp with Name context as fallback"),
658        }
659    }
660}