rusticity_term/ui/
sqs.rs

1use crate::aws::Region;
2use crate::common::{render_dropdown, InputFocus};
3use crate::keymap::Mode::FilterInput;
4use crate::sqs::pipe::{Column as PipeColumn, EventBridgePipe};
5use crate::sqs::queue::{Column as SqsColumn, Queue};
6use crate::sqs::sub::{Column as SubscriptionColumn, SnsSubscription};
7use crate::sqs::tag::{Column as TagColumn, QueueTag};
8use crate::sqs::trigger::{Column as TriggerColumn, LambdaTrigger};
9use crate::table::TableState;
10use crate::ui::filter::{
11    render_filter_bar, render_simple_filter, FilterConfig, FilterControl, SimpleFilterConfig,
12};
13use crate::ui::{labeled_field, render_tabs};
14use ratatui::widgets::*;
15
16pub const FILTER_CONTROLS: &[InputFocus] = &[InputFocus::Filter, InputFocus::Pagination];
17pub const SUBSCRIPTION_REGION: InputFocus = InputFocus::Dropdown("SubscriptionRegion");
18pub const SUBSCRIPTION_FILTER_CONTROLS: &[InputFocus] = &[
19    InputFocus::Filter,
20    SUBSCRIPTION_REGION,
21    InputFocus::Pagination,
22];
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum QueueDetailTab {
26    QueuePolicies,
27    Monitoring,
28    SnsSubscriptions,
29    LambdaTriggers,
30    EventBridgePipes,
31    DeadLetterQueue,
32    Tagging,
33    Encryption,
34    DeadLetterQueueRedriveTasks,
35}
36
37impl QueueDetailTab {
38    pub fn all() -> Vec<QueueDetailTab> {
39        vec![
40            QueueDetailTab::QueuePolicies,
41            QueueDetailTab::Monitoring,
42            QueueDetailTab::SnsSubscriptions,
43            QueueDetailTab::LambdaTriggers,
44            QueueDetailTab::EventBridgePipes,
45            QueueDetailTab::DeadLetterQueue,
46            QueueDetailTab::Tagging,
47            QueueDetailTab::Encryption,
48            QueueDetailTab::DeadLetterQueueRedriveTasks,
49        ]
50    }
51
52    pub fn name(&self) -> &'static str {
53        match self {
54            QueueDetailTab::QueuePolicies => "Queue policies",
55            QueueDetailTab::Monitoring => "Monitoring",
56            QueueDetailTab::SnsSubscriptions => "SNS subscriptions",
57            QueueDetailTab::LambdaTriggers => "Lambda triggers",
58            QueueDetailTab::EventBridgePipes => "EventBridge Pipes",
59            QueueDetailTab::Tagging => "Tagging",
60            QueueDetailTab::Encryption => "Encryption",
61            QueueDetailTab::DeadLetterQueueRedriveTasks => "Dead-letter queue redrive tasks",
62            QueueDetailTab::DeadLetterQueue => "Dead-letter queue",
63        }
64    }
65}
66
67impl crate::common::CyclicEnum for QueueDetailTab {
68    const ALL: &'static [Self] = &[
69        QueueDetailTab::QueuePolicies,
70        QueueDetailTab::Monitoring,
71        QueueDetailTab::SnsSubscriptions,
72        QueueDetailTab::LambdaTriggers,
73        QueueDetailTab::EventBridgePipes,
74        QueueDetailTab::DeadLetterQueue,
75        QueueDetailTab::Tagging,
76        QueueDetailTab::Encryption,
77        QueueDetailTab::DeadLetterQueueRedriveTasks,
78    ];
79}
80
81#[derive(Debug, Clone)]
82pub struct State {
83    pub queues: TableState<Queue>,
84    pub triggers: TableState<LambdaTrigger>,
85    pub trigger_visible_column_ids: Vec<String>,
86    pub trigger_column_ids: Vec<String>,
87    pub pipes: TableState<EventBridgePipe>,
88    pub pipe_visible_column_ids: Vec<String>,
89    pub pipe_column_ids: Vec<String>,
90    pub tags: TableState<QueueTag>,
91    pub tag_visible_column_ids: Vec<String>,
92    pub tag_column_ids: Vec<String>,
93    pub subscriptions: TableState<SnsSubscription>,
94    pub subscription_visible_column_ids: Vec<String>,
95    pub subscription_column_ids: Vec<String>,
96    pub subscription_region_filter: String,
97    pub subscription_region_selected: usize,
98    pub input_focus: InputFocus,
99    pub current_queue: Option<String>,
100    pub detail_tab: QueueDetailTab,
101    pub policy_scroll: usize,
102    pub policy_document: String,
103    pub metric_data: Vec<(i64, f64)>, // (timestamp, value) for ApproximateAgeOfOldestMessage
104    pub metric_data_delayed: Vec<(i64, f64)>, // (timestamp, value) for ApproximateNumberOfMessagesDelayed
105    pub metric_data_not_visible: Vec<(i64, f64)>, // (timestamp, value) for ApproximateNumberOfMessagesNotVisible
106    pub metric_data_visible: Vec<(i64, f64)>, // (timestamp, value) for ApproximateNumberOfMessagesVisible
107    pub metric_data_empty_receives: Vec<(i64, f64)>, // (timestamp, value) for NumberOfEmptyReceives
108    pub metric_data_messages_deleted: Vec<(i64, f64)>, // (timestamp, value) for NumberOfMessagesDeleted
109    pub metric_data_messages_received: Vec<(i64, f64)>, // (timestamp, value) for NumberOfMessagesReceived
110    pub metric_data_messages_sent: Vec<(i64, f64)>, // (timestamp, value) for NumberOfMessagesSent
111    pub metric_data_sent_message_size: Vec<(i64, f64)>, // (timestamp, value) for SentMessageSize
112    pub metrics_loading: bool,
113    pub monitoring_scroll: usize,
114}
115
116impl Default for State {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl State {
123    pub fn new() -> Self {
124        let trigger_column_ids: Vec<String> = TriggerColumn::ids()
125            .into_iter()
126            .map(|s| s.to_string())
127            .collect();
128        let pipe_column_ids: Vec<String> = PipeColumn::ids()
129            .into_iter()
130            .map(|s| s.to_string())
131            .collect();
132        let tag_column_ids: Vec<String> = TagColumn::ids()
133            .into_iter()
134            .map(|s| s.to_string())
135            .collect();
136        let subscription_column_ids: Vec<String> = SubscriptionColumn::ids()
137            .into_iter()
138            .map(|s| s.to_string())
139            .collect();
140        Self {
141            queues: TableState::new(),
142            triggers: TableState::new(),
143            trigger_visible_column_ids: trigger_column_ids.clone(),
144            trigger_column_ids,
145            pipes: TableState::new(),
146            pipe_visible_column_ids: pipe_column_ids.clone(),
147            pipe_column_ids,
148            tags: TableState::new(),
149            tag_visible_column_ids: tag_column_ids.clone(),
150            tag_column_ids,
151            subscriptions: TableState::new(),
152            subscription_visible_column_ids: subscription_column_ids.clone(),
153            subscription_column_ids,
154            subscription_region_filter: String::new(),
155            subscription_region_selected: 0,
156            input_focus: InputFocus::Filter,
157            current_queue: None,
158            detail_tab: QueueDetailTab::QueuePolicies,
159            policy_scroll: 0,
160            policy_document: r#"{
161  "Version": "2012-10-17",
162  "Statement": [
163    {
164      "Effect": "Allow",
165      "Principal": "*",
166      "Action": "sqs:*",
167      "Resource": "*"
168    }
169  ]
170}"#
171            .to_string(),
172            metric_data: Vec::new(),
173            metric_data_delayed: Vec::new(),
174            metric_data_not_visible: Vec::new(),
175            metric_data_visible: Vec::new(),
176            metric_data_empty_receives: Vec::new(),
177            metric_data_messages_deleted: Vec::new(),
178            metric_data_messages_received: Vec::new(),
179            metric_data_messages_sent: Vec::new(),
180            metric_data_sent_message_size: Vec::new(),
181            metrics_loading: false,
182            monitoring_scroll: 0,
183        }
184    }
185}
186
187use crate::ui::monitoring::MonitoringState;
188
189impl MonitoringState for State {
190    fn is_metrics_loading(&self) -> bool {
191        self.metrics_loading
192    }
193
194    fn set_metrics_loading(&mut self, loading: bool) {
195        self.metrics_loading = loading;
196    }
197
198    fn monitoring_scroll(&self) -> usize {
199        self.monitoring_scroll
200    }
201
202    fn set_monitoring_scroll(&mut self, scroll: usize) {
203        self.monitoring_scroll = scroll;
204    }
205
206    fn clear_metrics(&mut self) {
207        self.metric_data.clear();
208        self.metric_data_delayed.clear();
209        self.metric_data_not_visible.clear();
210        self.metric_data_visible.clear();
211        self.metric_data_empty_receives.clear();
212        self.metric_data_messages_deleted.clear();
213        self.metric_data_messages_received.clear();
214        self.metric_data_messages_sent.clear();
215        self.metric_data_sent_message_size.clear();
216    }
217}
218
219pub fn filtered_queues<'a>(queues: &'a [Queue], filter: &str) -> Vec<&'a Queue> {
220    queues
221        .iter()
222        .filter(|q| filter.is_empty() || q.name.to_lowercase().starts_with(&filter.to_lowercase()))
223        .collect()
224}
225
226pub fn filtered_lambda_triggers(app: &crate::App) -> Vec<&crate::sqs::LambdaTrigger> {
227    let mut filtered: Vec<_> = app
228        .sqs_state
229        .triggers
230        .items
231        .iter()
232        .filter(|t| {
233            app.sqs_state.triggers.filter.is_empty()
234                || t.uuid
235                    .to_lowercase()
236                    .contains(&app.sqs_state.triggers.filter.to_lowercase())
237                || t.arn
238                    .to_lowercase()
239                    .contains(&app.sqs_state.triggers.filter.to_lowercase())
240        })
241        .collect();
242
243    // Sort by last_modified ASC
244    filtered.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
245    filtered
246}
247
248pub fn filtered_tags(app: &crate::App) -> Vec<&QueueTag> {
249    let mut filtered: Vec<_> = app
250        .sqs_state
251        .tags
252        .items
253        .iter()
254        .filter(|t| {
255            app.sqs_state.tags.filter.is_empty()
256                || t.key
257                    .to_lowercase()
258                    .contains(&app.sqs_state.tags.filter.to_lowercase())
259                || t.value
260                    .to_lowercase()
261                    .contains(&app.sqs_state.tags.filter.to_lowercase())
262        })
263        .collect();
264
265    // Sort by value ASC
266    filtered.sort_by(|a, b| a.value.cmp(&b.value));
267    filtered
268}
269
270pub fn filtered_subscriptions(app: &crate::App) -> Vec<&SnsSubscription> {
271    let region_filter = if app.sqs_state.subscription_region_filter.is_empty() {
272        &app.region
273    } else {
274        &app.sqs_state.subscription_region_filter
275    };
276
277    let mut filtered: Vec<_> = app
278        .sqs_state
279        .subscriptions
280        .items
281        .iter()
282        .filter(|s| {
283            let text_match = app.sqs_state.subscriptions.filter.is_empty()
284                || s.subscription_arn
285                    .to_lowercase()
286                    .contains(&app.sqs_state.subscriptions.filter.to_lowercase())
287                || s.topic_arn
288                    .to_lowercase()
289                    .contains(&app.sqs_state.subscriptions.filter.to_lowercase());
290
291            let region_match = s.subscription_arn.contains(region_filter);
292
293            text_match && region_match
294        })
295        .collect();
296
297    // Sort by subscription_arn ASC
298    filtered.sort_by(|a, b| a.subscription_arn.cmp(&b.subscription_arn));
299    filtered
300}
301
302pub fn filtered_eventbridge_pipes(app: &crate::App) -> Vec<&crate::sqs::EventBridgePipe> {
303    let mut filtered: Vec<_> = app
304        .sqs_state
305        .pipes
306        .items
307        .iter()
308        .filter(|p| {
309            app.sqs_state.pipes.filter.is_empty()
310                || p.name
311                    .to_lowercase()
312                    .contains(&app.sqs_state.pipes.filter.to_lowercase())
313                || p.target
314                    .to_lowercase()
315                    .contains(&app.sqs_state.pipes.filter.to_lowercase())
316        })
317        .collect();
318
319    filtered.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
320    filtered
321}
322
323pub async fn load_sqs_queues(app: &mut crate::App) -> anyhow::Result<()> {
324    let queues = app.sqs_client.list_queues("").await?;
325    app.sqs_state.queues.items = queues
326        .into_iter()
327        .map(|q| Queue {
328            name: q.name,
329            url: q.url,
330            queue_type: q.queue_type,
331            created_timestamp: q.created_timestamp,
332            messages_available: q.messages_available,
333            messages_in_flight: q.messages_in_flight,
334            encryption: q.encryption,
335            content_based_deduplication: q.content_based_deduplication,
336            last_modified_timestamp: q.last_modified_timestamp,
337            visibility_timeout: q.visibility_timeout,
338            message_retention_period: q.message_retention_period,
339            maximum_message_size: q.maximum_message_size,
340            delivery_delay: q.delivery_delay,
341            receive_message_wait_time: q.receive_message_wait_time,
342            high_throughput_fifo: q.high_throughput_fifo,
343            deduplication_scope: q.deduplication_scope,
344            fifo_throughput_limit: q.fifo_throughput_limit,
345            dead_letter_queue: q.dead_letter_queue,
346            messages_delayed: q.messages_delayed,
347            redrive_allow_policy: q.redrive_allow_policy,
348            redrive_policy: q.redrive_policy,
349            redrive_task_id: q.redrive_task_id,
350            redrive_task_start_time: q.redrive_task_start_time,
351            redrive_task_status: q.redrive_task_status,
352            redrive_task_percent: q.redrive_task_percent,
353            redrive_task_destination: q.redrive_task_destination,
354        })
355        .collect();
356    Ok(())
357}
358
359pub async fn load_lambda_triggers(app: &mut crate::App, queue_url: &str) -> anyhow::Result<()> {
360    let queue_arn = app.sqs_client.get_queue_arn(queue_url).await?;
361    let triggers = app.sqs_client.list_lambda_triggers(&queue_arn).await?;
362
363    app.sqs_state.triggers.items = triggers
364        .into_iter()
365        .map(|t| LambdaTrigger {
366            uuid: t.uuid,
367            arn: t.arn,
368            status: t.status,
369            last_modified: t.last_modified,
370        })
371        .collect();
372
373    // Sort by last_modified ascending (oldest first)
374    app.sqs_state
375        .triggers
376        .items
377        .sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
378
379    Ok(())
380}
381
382pub async fn load_metrics(app: &mut crate::App, queue_name: &str) -> anyhow::Result<()> {
383    let metrics = app.sqs_client.get_queue_metrics(queue_name).await?;
384    app.sqs_state.metric_data = metrics;
385
386    let delayed_metrics = app.sqs_client.get_queue_delayed_metrics(queue_name).await?;
387    app.sqs_state.metric_data_delayed = delayed_metrics;
388
389    let not_visible_metrics = app
390        .sqs_client
391        .get_queue_not_visible_metrics(queue_name)
392        .await?;
393    app.sqs_state.metric_data_not_visible = not_visible_metrics;
394
395    let visible_metrics = app.sqs_client.get_queue_visible_metrics(queue_name).await?;
396    app.sqs_state.metric_data_visible = visible_metrics;
397
398    let empty_receives_metrics = app
399        .sqs_client
400        .get_queue_empty_receives_metrics(queue_name)
401        .await?;
402    app.sqs_state.metric_data_empty_receives = empty_receives_metrics;
403
404    let messages_deleted_metrics = app
405        .sqs_client
406        .get_queue_messages_deleted_metrics(queue_name)
407        .await?;
408    app.sqs_state.metric_data_messages_deleted = messages_deleted_metrics;
409
410    let messages_received_metrics = app
411        .sqs_client
412        .get_queue_messages_received_metrics(queue_name)
413        .await?;
414    app.sqs_state.metric_data_messages_received = messages_received_metrics;
415
416    let messages_sent_metrics = app
417        .sqs_client
418        .get_queue_messages_sent_metrics(queue_name)
419        .await?;
420    app.sqs_state.metric_data_messages_sent = messages_sent_metrics;
421
422    let sent_message_size_metrics = app
423        .sqs_client
424        .get_queue_sent_message_size_metrics(queue_name)
425        .await?;
426    app.sqs_state.metric_data_sent_message_size = sent_message_size_metrics;
427
428    Ok(())
429}
430
431pub async fn load_pipes(app: &mut crate::App, queue_url: &str) -> anyhow::Result<()> {
432    let queue_arn = app.sqs_client.get_queue_arn(queue_url).await?;
433    let pipes = app.sqs_client.list_pipes(&queue_arn).await?;
434
435    app.sqs_state.pipes.items = pipes
436        .into_iter()
437        .map(|p| EventBridgePipe {
438            name: p.name,
439            status: p.status,
440            target: p.target,
441            last_modified: p.last_modified,
442        })
443        .collect();
444
445    app.sqs_state
446        .pipes
447        .items
448        .sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
449
450    Ok(())
451}
452
453pub async fn load_tags(app: &mut crate::App, queue_url: &str) -> anyhow::Result<()> {
454    let tags = app.sqs_client.list_tags(queue_url).await?;
455
456    app.sqs_state.tags.items = tags
457        .into_iter()
458        .map(|t| QueueTag {
459            key: t.key,
460            value: t.value,
461        })
462        .collect();
463
464    app.sqs_state
465        .tags
466        .items
467        .sort_by(|a, b| a.value.cmp(&b.value));
468
469    Ok(())
470}
471
472pub fn render_queues(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
473    use ratatui::widgets::Clear;
474
475    frame.render_widget(Clear, area);
476
477    if app.sqs_state.current_queue.is_some() {
478        render_queue_detail(frame, app, area);
479    } else {
480        render_queue_list(frame, app, area);
481    }
482}
483
484fn render_queue_detail(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
485    use ratatui::prelude::*;
486    use ratatui::widgets::{Clear, Paragraph};
487
488    frame.render_widget(Clear, area);
489
490    let queue = app
491        .sqs_state
492        .queues
493        .items
494        .iter()
495        .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
496
497    let queue_name = queue.map(|q| q.name.as_str()).unwrap_or("Unknown");
498
499    let details_height = queue.map_or(3, |q| {
500        let field_count = render_details_fields(q).len();
501        field_count as u16 + 2 // fields + 2 borders
502    });
503
504    let chunks = Layout::default()
505        .direction(Direction::Vertical)
506        .constraints([
507            Constraint::Length(1),              // Queue name
508            Constraint::Length(details_height), // Details (dynamic)
509            Constraint::Length(1),              // Tabs
510            Constraint::Min(0),                 // Tab content
511        ])
512        .split(area);
513
514    // Queue name header
515    let header = Paragraph::new(queue_name).style(
516        Style::default()
517            .fg(Color::Yellow)
518            .add_modifier(Modifier::BOLD),
519    );
520    frame.render_widget(header, chunks[0]);
521
522    // Details pane
523    if let Some(q) = queue {
524        render_details_pane(frame, q, chunks[1]);
525    }
526
527    // Tabs
528    // Tabs - generated from QueueDetailTab::all()
529    let tabs: Vec<(&str, QueueDetailTab)> = QueueDetailTab::all()
530        .into_iter()
531        .map(|tab| (tab.name(), tab))
532        .collect();
533
534    render_tabs(frame, chunks[2], &tabs, &app.sqs_state.detail_tab);
535
536    // Tab content
537    match app.sqs_state.detail_tab {
538        QueueDetailTab::QueuePolicies => {
539            render_queue_policies_tab(frame, app, chunks[3]);
540        }
541        QueueDetailTab::Monitoring => {
542            if app.sqs_state.metrics_loading {
543                let loading_block = Block::default()
544                    .title(" Monitoring ")
545                    .borders(Borders::ALL)
546                    .border_type(BorderType::Rounded);
547                let loading_text = Paragraph::new("Loading metrics...")
548                    .block(loading_block)
549                    .alignment(ratatui::layout::Alignment::Center);
550                frame.render_widget(loading_text, chunks[3]);
551            } else {
552                use crate::ui::monitoring::{render_monitoring_tab, MetricChart};
553
554                let age_max: f64 = app
555                    .sqs_state
556                    .metric_data
557                    .iter()
558                    .map(|(_, v)| v)
559                    .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
560                let age_label = format!(
561                    "Age [max: {}]",
562                    if age_max.is_finite() {
563                        format!("{:.0}s", age_max)
564                    } else {
565                        "--".to_string()
566                    }
567                );
568
569                render_monitoring_tab(
570                    frame,
571                    chunks[3],
572                    &[
573                        MetricChart {
574                            title: "Approximate age of oldest message",
575                            data: &app.sqs_state.metric_data,
576                            y_axis_label: "Seconds",
577                            x_axis_label: Some(age_label),
578                        },
579                        MetricChart {
580                            title: "Approximate number of messages delayed",
581                            data: &app.sqs_state.metric_data_delayed,
582                            y_axis_label: "Count",
583                            x_axis_label: None,
584                        },
585                        MetricChart {
586                            title: "Approximate number of messages not visible",
587                            data: &app.sqs_state.metric_data_not_visible,
588                            y_axis_label: "Count",
589                            x_axis_label: None,
590                        },
591                        MetricChart {
592                            title: "Approximate number of messages visible",
593                            data: &app.sqs_state.metric_data_visible,
594                            y_axis_label: "Count",
595                            x_axis_label: None,
596                        },
597                        MetricChart {
598                            title: "Number of empty receives",
599                            data: &app.sqs_state.metric_data_empty_receives,
600                            y_axis_label: "Count",
601                            x_axis_label: None,
602                        },
603                        MetricChart {
604                            title: "Number of messages deleted",
605                            data: &app.sqs_state.metric_data_messages_deleted,
606                            y_axis_label: "Count",
607                            x_axis_label: None,
608                        },
609                        MetricChart {
610                            title: "Number of messages received",
611                            data: &app.sqs_state.metric_data_messages_received,
612                            y_axis_label: "Count",
613                            x_axis_label: None,
614                        },
615                        MetricChart {
616                            title: "Number of messages sent",
617                            data: &app.sqs_state.metric_data_messages_sent,
618                            y_axis_label: "Count",
619                            x_axis_label: None,
620                        },
621                        MetricChart {
622                            title: "Sent message size",
623                            data: &app.sqs_state.metric_data_sent_message_size,
624                            y_axis_label: "Bytes",
625                            x_axis_label: None,
626                        },
627                    ],
628                    &[],
629                    &[],
630                    &[],
631                    app.sqs_state.monitoring_scroll,
632                );
633            }
634        }
635        QueueDetailTab::SnsSubscriptions => {
636            render_subscriptions_tab(frame, app, chunks[3]);
637        }
638        QueueDetailTab::LambdaTriggers => {
639            render_lambda_triggers_tab(frame, app, chunks[3]);
640        }
641        QueueDetailTab::EventBridgePipes => {
642            render_eventbridge_pipes_tab(frame, app, chunks[3]);
643        }
644        QueueDetailTab::DeadLetterQueue => {
645            render_dead_letter_queue_tab(frame, app, chunks[3]);
646        }
647        QueueDetailTab::Tagging => {
648            render_tags_tab(frame, app, chunks[3]);
649        }
650        QueueDetailTab::Encryption => {
651            render_encryption_tab(frame, app, chunks[3]);
652        }
653        QueueDetailTab::DeadLetterQueueRedriveTasks => {
654            render_dlq_redrive_tasks_tab(frame, app, chunks[3]);
655        }
656    }
657}
658
659fn render_details_fields(queue: &Queue) -> Vec<ratatui::text::Line<'static>> {
660    let max_msg_size = queue
661        .maximum_message_size
662        .split_whitespace()
663        .next()
664        .and_then(|s| s.parse::<i64>().ok())
665        .map(crate::common::format_bytes)
666        .unwrap_or_else(|| queue.maximum_message_size.clone());
667
668    let retention_period = queue
669        .message_retention_period
670        .parse::<i32>()
671        .ok()
672        .map(crate::common::format_duration_seconds)
673        .unwrap_or_else(|| queue.message_retention_period.clone());
674
675    let visibility_timeout = queue
676        .visibility_timeout
677        .parse::<i32>()
678        .ok()
679        .map(crate::common::format_duration_seconds)
680        .unwrap_or_else(|| queue.visibility_timeout.clone());
681
682    let delivery_delay = queue
683        .delivery_delay
684        .parse::<i32>()
685        .ok()
686        .map(crate::common::format_duration_seconds)
687        .unwrap_or_else(|| queue.delivery_delay.clone());
688
689    let receive_wait_time = queue
690        .receive_message_wait_time
691        .parse::<i32>()
692        .ok()
693        .map(crate::common::format_duration_seconds)
694        .unwrap_or_else(|| queue.receive_message_wait_time.clone());
695
696    vec![
697        labeled_field("Name", &queue.name),
698        labeled_field("Type", &queue.queue_type),
699        labeled_field(
700            "ARN",
701            format!(
702                "arn:aws:sqs:{}:{}:{}",
703                extract_region(&queue.url),
704                extract_account_id(&queue.url),
705                queue.name
706            ),
707        ),
708        labeled_field("Encryption", &queue.encryption),
709        labeled_field("URL", &queue.url),
710        labeled_field("Dead-letter queue", &queue.dead_letter_queue),
711        labeled_field(
712            "Created",
713            crate::common::format_unix_timestamp(&queue.created_timestamp),
714        ),
715        labeled_field("Maximum message size", max_msg_size),
716        labeled_field(
717            "Last updated",
718            crate::common::format_unix_timestamp(&queue.last_modified_timestamp),
719        ),
720        labeled_field("Message retention period", retention_period),
721        labeled_field("Default visibility timeout", visibility_timeout),
722        labeled_field("Messages available", &queue.messages_available),
723        labeled_field("Delivery delay", delivery_delay),
724        labeled_field(
725            "Messages in flight (not available to other consumers)",
726            &queue.messages_in_flight,
727        ),
728        labeled_field("Receive message wait time", receive_wait_time),
729        labeled_field("Messages delayed", &queue.messages_delayed),
730        labeled_field(
731            "Content-based deduplication",
732            &queue.content_based_deduplication,
733        ),
734        labeled_field("High throughput FIFO", &queue.high_throughput_fifo),
735        labeled_field("Deduplication scope", &queue.deduplication_scope),
736        labeled_field("FIFO throughput limit", &queue.fifo_throughput_limit),
737        labeled_field("Redrive allow policy", &queue.redrive_allow_policy),
738    ]
739}
740
741fn render_details_pane(frame: &mut ratatui::Frame, queue: &Queue, area: ratatui::prelude::Rect) {
742    use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
743
744    let block = Block::default()
745        .title(" Details ")
746        .borders(Borders::ALL)
747        .border_type(BorderType::Rounded)
748        .border_style(crate::ui::active_border());
749
750    let inner = block.inner(area);
751    frame.render_widget(block, area);
752
753    let lines = render_details_fields(queue);
754    let paragraph = Paragraph::new(lines);
755    frame.render_widget(paragraph, inner);
756}
757
758fn render_queue_policies_tab(
759    frame: &mut ratatui::Frame,
760    app: &crate::App,
761    area: ratatui::prelude::Rect,
762) {
763    use ratatui::prelude::{Constraint, Direction, Layout};
764
765    let chunks = Layout::default()
766        .direction(Direction::Vertical)
767        .constraints([Constraint::Min(0)])
768        .split(area);
769
770    // Access policy JSON using common JSON renderer
771    crate::ui::render_json_highlighted(
772        frame,
773        chunks[0],
774        &app.sqs_state.policy_document,
775        app.sqs_state.policy_scroll,
776        " Access policy ",
777        true,
778    );
779}
780
781fn render_lambda_triggers_tab(
782    frame: &mut ratatui::Frame,
783    app: &crate::App,
784    area: ratatui::prelude::Rect,
785) {
786    use crate::ui::table::{render_table, Column, TableConfig};
787    use ratatui::prelude::*;
788
789    let chunks = Layout::default()
790        .direction(Direction::Vertical)
791        .constraints([Constraint::Length(3), Constraint::Min(0)])
792        .split(area);
793
794    let filtered = filtered_lambda_triggers(app);
795
796    let columns: Vec<Box<dyn Column<crate::sqs::LambdaTrigger>>> = app
797        .sqs_state
798        .trigger_visible_column_ids
799        .iter()
800        .filter_map(|id| TriggerColumn::from_id(id))
801        .map(|col| Box::new(col) as Box<dyn Column<crate::sqs::LambdaTrigger>>)
802        .collect();
803
804    // Pagination
805    let page_size = app.sqs_state.triggers.page_size.value();
806    let total_pages = filtered.len().div_ceil(page_size.max(1));
807    let current_page = app.sqs_state.triggers.selected / page_size.max(1);
808    let pagination = crate::ui::render_pagination_text(current_page, total_pages);
809
810    // Filter at top
811    render_simple_filter(
812        frame,
813        chunks[0],
814        SimpleFilterConfig {
815            filter_text: &app.sqs_state.triggers.filter,
816            placeholder: "Search triggers",
817            pagination: &pagination,
818            mode: app.mode,
819            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
820            is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
821        },
822    );
823
824    let start_idx = current_page * page_size;
825    let end_idx = (start_idx + page_size).min(filtered.len());
826    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
827
828    let expanded_index = app.sqs_state.triggers.expanded_item.and_then(|idx| {
829        if idx >= start_idx && idx < end_idx {
830            Some(idx - start_idx)
831        } else {
832            None
833        }
834    });
835
836    render_table(
837        frame,
838        TableConfig {
839            area: chunks[1],
840            columns: &columns,
841            items: paginated,
842            selected_index: app.sqs_state.triggers.selected % page_size.max(1),
843            is_active: app.mode != crate::keymap::Mode::FilterInput,
844            title: format!(" Lambda triggers ({}) ", filtered.len()),
845            sort_column: "last_modified",
846            sort_direction: crate::common::SortDirection::Asc,
847            expanded_index,
848            get_expanded_content: Some(Box::new(|trigger: &crate::sqs::LambdaTrigger| {
849                crate::ui::table::expanded_from_columns(&columns, trigger)
850            })),
851        },
852    );
853}
854
855pub fn extract_region(url: &str) -> &str {
856    url.split("sqs.")
857        .nth(1)
858        .and_then(|s| s.split('.').next())
859        .unwrap_or("unknown")
860}
861
862pub fn extract_account_id(url: &str) -> &str {
863    url.split('/').nth(3).unwrap_or("unknown")
864}
865
866fn render_eventbridge_pipes_tab(
867    frame: &mut ratatui::Frame,
868    app: &crate::App,
869    area: ratatui::prelude::Rect,
870) {
871    use crate::ui::table::{render_table, Column, TableConfig};
872    use ratatui::prelude::*;
873
874    let chunks = Layout::default()
875        .direction(Direction::Vertical)
876        .constraints([Constraint::Length(3), Constraint::Min(0)])
877        .split(area);
878
879    let filtered = filtered_eventbridge_pipes(app);
880
881    let columns: Vec<Box<dyn Column<crate::sqs::EventBridgePipe>>> = app
882        .sqs_state
883        .pipe_visible_column_ids
884        .iter()
885        .filter_map(|id| PipeColumn::from_id(id))
886        .map(|col| Box::new(col) as Box<dyn Column<crate::sqs::EventBridgePipe>>)
887        .collect();
888
889    let page_size = app.sqs_state.pipes.page_size.value();
890    let total_pages = filtered.len().div_ceil(page_size.max(1));
891    let current_page = app.sqs_state.pipes.selected / page_size.max(1);
892    let pagination = crate::ui::render_pagination_text(current_page, total_pages);
893
894    render_simple_filter(
895        frame,
896        chunks[0],
897        SimpleFilterConfig {
898            filter_text: &app.sqs_state.pipes.filter,
899            placeholder: "Search pipes",
900            pagination: &pagination,
901            mode: app.mode,
902            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
903            is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
904        },
905    );
906
907    let start_idx = current_page * page_size;
908    let end_idx = (start_idx + page_size).min(filtered.len());
909    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
910
911    let expanded_index = app.sqs_state.pipes.expanded_item.and_then(|idx| {
912        if idx >= start_idx && idx < end_idx {
913            Some(idx - start_idx)
914        } else {
915            None
916        }
917    });
918
919    render_table(
920        frame,
921        TableConfig {
922            area: chunks[1],
923            columns: &columns,
924            items: paginated,
925            selected_index: app.sqs_state.pipes.selected % page_size.max(1),
926            is_active: app.mode != crate::keymap::Mode::FilterInput,
927            title: format!(" EventBridge Pipes ({}) ", filtered.len()),
928            sort_column: "last_modified",
929            sort_direction: crate::common::SortDirection::Asc,
930            expanded_index,
931            get_expanded_content: Some(Box::new(|pipe: &crate::sqs::EventBridgePipe| {
932                crate::ui::table::expanded_from_columns(&columns, pipe)
933            })),
934        },
935    );
936}
937
938fn render_tags_tab(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
939    use crate::ui::table::{render_table, Column, TableConfig};
940    use ratatui::prelude::*;
941
942    let chunks = Layout::default()
943        .direction(Direction::Vertical)
944        .constraints([Constraint::Length(3), Constraint::Min(0)])
945        .split(area);
946
947    let filtered = filtered_tags(app);
948
949    let columns: Vec<Box<dyn Column<QueueTag>>> = app
950        .sqs_state
951        .tag_visible_column_ids
952        .iter()
953        .filter_map(|id| TagColumn::from_id(id))
954        .map(|col| Box::new(col) as Box<dyn Column<QueueTag>>)
955        .collect();
956
957    let page_size = app.sqs_state.tags.page_size.value();
958    let total_pages = filtered.len().div_ceil(page_size.max(1));
959    let current_page = app.sqs_state.tags.selected / page_size.max(1);
960    let pagination = crate::ui::render_pagination_text(current_page, total_pages);
961
962    render_simple_filter(
963        frame,
964        chunks[0],
965        SimpleFilterConfig {
966            filter_text: &app.sqs_state.tags.filter,
967            placeholder: "Search tags",
968            pagination: &pagination,
969            mode: app.mode,
970            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
971            is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
972        },
973    );
974
975    let start_idx = current_page * page_size;
976    let end_idx = (start_idx + page_size).min(filtered.len());
977    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
978
979    let expanded_index = app.sqs_state.tags.expanded_item.and_then(|idx| {
980        if idx >= start_idx && idx < end_idx {
981            Some(idx - start_idx)
982        } else {
983            None
984        }
985    });
986
987    render_table(
988        frame,
989        TableConfig {
990            area: chunks[1],
991            columns: &columns,
992            items: paginated,
993            selected_index: app.sqs_state.tags.selected % page_size.max(1),
994            is_active: app.mode != crate::keymap::Mode::FilterInput,
995            title: format!(" Tagging ({}) ", filtered.len()),
996            sort_column: "value",
997            sort_direction: crate::common::SortDirection::Asc,
998            expanded_index,
999            get_expanded_content: Some(Box::new(|tag: &QueueTag| {
1000                crate::ui::table::expanded_from_columns(&columns, tag)
1001            })),
1002        },
1003    );
1004}
1005
1006fn render_subscriptions_tab(
1007    frame: &mut ratatui::Frame,
1008    app: &crate::App,
1009    area: ratatui::prelude::Rect,
1010) {
1011    use crate::ui::table::{render_table, Column, TableConfig};
1012    use ratatui::prelude::*;
1013
1014    let chunks = Layout::default()
1015        .direction(Direction::Vertical)
1016        .constraints([Constraint::Length(3), Constraint::Min(0)])
1017        .split(area);
1018
1019    let filtered = filtered_subscriptions(app);
1020
1021    let columns: Vec<Box<dyn Column<SnsSubscription>>> = app
1022        .sqs_state
1023        .subscription_visible_column_ids
1024        .iter()
1025        .filter_map(|id| SubscriptionColumn::from_id(id))
1026        .map(|col| Box::new(col) as Box<dyn Column<SnsSubscription>>)
1027        .collect();
1028
1029    let page_size = app.sqs_state.subscriptions.page_size.value();
1030    let total_pages = filtered.len().div_ceil(page_size.max(1));
1031    let current_page = app.sqs_state.subscriptions.selected / page_size.max(1);
1032    let pagination = crate::ui::render_pagination_text(current_page, total_pages);
1033
1034    // Render filter with region dropdown
1035    render_subscription_filter(frame, app, chunks[0], &pagination);
1036
1037    let start_idx = current_page * page_size;
1038    let end_idx = (start_idx + page_size).min(filtered.len());
1039    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1040
1041    let expanded_index = app.sqs_state.subscriptions.expanded_item.and_then(|idx| {
1042        if idx >= start_idx && idx < end_idx {
1043            Some(idx - start_idx)
1044        } else {
1045            None
1046        }
1047    });
1048
1049    render_table(
1050        frame,
1051        TableConfig {
1052            area: chunks[1],
1053            columns: &columns,
1054            items: paginated,
1055            selected_index: app.sqs_state.subscriptions.selected % page_size.max(1),
1056            is_active: app.mode != crate::keymap::Mode::FilterInput,
1057            title: format!(" SNS subscriptions ({}) ", filtered.len()),
1058            sort_column: "subscription_arn",
1059            sort_direction: crate::common::SortDirection::Asc,
1060            expanded_index,
1061            get_expanded_content: Some(Box::new(|sub: &SnsSubscription| {
1062                crate::ui::table::expanded_from_columns(&columns, sub)
1063            })),
1064        },
1065    );
1066
1067    // Render region dropdown if focused (after table so it appears on top)
1068    if app.mode == FilterInput && app.sqs_state.input_focus == SUBSCRIPTION_REGION {
1069        let regions = Region::all();
1070        let region_codes: Vec<&str> = regions.iter().map(|r| r.code).collect();
1071        render_dropdown(
1072            frame,
1073            &region_codes,
1074            app.sqs_state.subscription_region_selected,
1075            chunks[0],
1076            pagination.len() as u16 + 3, // pagination + separator
1077        );
1078    }
1079}
1080
1081fn render_subscription_filter(
1082    frame: &mut ratatui::Frame,
1083    app: &crate::App,
1084    area: ratatui::prelude::Rect,
1085    pagination: &str,
1086) {
1087    let region_text = if app.sqs_state.subscription_region_filter.is_empty() {
1088        format!("Subscription region: {}", app.region)
1089    } else {
1090        format!(
1091            "Subscription region: {}",
1092            app.sqs_state.subscription_region_filter
1093        )
1094    };
1095
1096    render_filter_bar(
1097        frame,
1098        FilterConfig {
1099            filter_text: &app.sqs_state.subscriptions.filter,
1100            placeholder: "Search subscriptions",
1101            mode: app.mode,
1102            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1103            controls: vec![
1104                FilterControl {
1105                    text: region_text,
1106                    is_focused: app.sqs_state.input_focus == SUBSCRIPTION_REGION,
1107                },
1108                FilterControl {
1109                    text: pagination.to_string(),
1110                    is_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1111                },
1112            ],
1113            area,
1114        },
1115    );
1116}
1117
1118fn render_dead_letter_queue_tab(
1119    frame: &mut ratatui::Frame,
1120    app: &crate::App,
1121    area: ratatui::prelude::Rect,
1122) {
1123    use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
1124
1125    let queue = app
1126        .sqs_state
1127        .queues
1128        .items
1129        .iter()
1130        .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1131
1132    let block = Block::default()
1133        .title(" Dead-letter queue ")
1134        .borders(Borders::ALL)
1135        .border_type(BorderType::Rounded)
1136        .border_style(crate::ui::active_border());
1137
1138    let inner = block.inner(area);
1139    frame.render_widget(block, area);
1140
1141    if let Some(q) = queue {
1142        if !q.redrive_policy.is_empty() {
1143            // Parse RedrivePolicy JSON
1144            if let Ok(policy) = serde_json::from_str::<serde_json::Value>(&q.redrive_policy) {
1145                let dlq_arn = policy
1146                    .get("deadLetterTargetArn")
1147                    .and_then(|v| v.as_str())
1148                    .unwrap_or("-");
1149                let max_receives = policy
1150                    .get("maxReceiveCount")
1151                    .and_then(|v| v.as_i64())
1152                    .map(|n| n.to_string())
1153                    .unwrap_or_else(|| "-".to_string());
1154
1155                let lines = vec![
1156                    labeled_field("Queue", dlq_arn),
1157                    labeled_field("Maximum receives", &max_receives),
1158                ];
1159
1160                let paragraph = Paragraph::new(lines);
1161                frame.render_widget(paragraph, inner);
1162            } else {
1163                let paragraph = Paragraph::new("No dead-letter queue configured");
1164                frame.render_widget(paragraph, inner);
1165            }
1166        } else {
1167            let paragraph = Paragraph::new("No dead-letter queue configured");
1168            frame.render_widget(paragraph, inner);
1169        }
1170    }
1171}
1172
1173fn render_encryption_tab(
1174    frame: &mut ratatui::Frame,
1175    app: &crate::App,
1176    area: ratatui::prelude::Rect,
1177) {
1178    use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
1179
1180    let queue = app
1181        .sqs_state
1182        .queues
1183        .items
1184        .iter()
1185        .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1186
1187    let block = Block::default()
1188        .title(" Encryption ")
1189        .borders(Borders::ALL)
1190        .border_type(BorderType::Rounded)
1191        .border_style(crate::ui::active_border());
1192
1193    let inner = block.inner(area);
1194    frame.render_widget(block, area);
1195
1196    if let Some(q) = queue {
1197        let encryption_text = if q.encryption.is_empty() || q.encryption == "-" {
1198            "Server-side encryption is not enabled".to_string()
1199        } else {
1200            format!("Server-side encryption is managed by {}", q.encryption)
1201        };
1202
1203        let paragraph = Paragraph::new(encryption_text);
1204        frame.render_widget(paragraph, inner);
1205    }
1206}
1207
1208fn render_dlq_redrive_tasks_tab(
1209    frame: &mut ratatui::Frame,
1210    app: &crate::App,
1211    area: ratatui::prelude::Rect,
1212) {
1213    use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
1214
1215    let queue = app
1216        .sqs_state
1217        .queues
1218        .items
1219        .iter()
1220        .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1221
1222    let block = Block::default()
1223        .title(" Dead-letter queue redrive status ")
1224        .borders(Borders::ALL)
1225        .border_type(BorderType::Rounded)
1226        .border_style(crate::ui::active_border());
1227
1228    let inner = block.inner(area);
1229    frame.render_widget(block, area);
1230
1231    if let Some(q) = queue {
1232        let lines = vec![
1233            labeled_field("Name", &q.redrive_task_id),
1234            labeled_field("Date started", &q.redrive_task_start_time),
1235            labeled_field("Percent processed", &q.redrive_task_percent),
1236            labeled_field("Status", &q.redrive_task_status),
1237            labeled_field("Redrive destination", &q.redrive_task_destination),
1238        ];
1239
1240        let paragraph = Paragraph::new(lines);
1241        frame.render_widget(paragraph, inner);
1242    }
1243}
1244
1245fn render_queue_list(frame: &mut ratatui::Frame, app: &crate::App, area: ratatui::prelude::Rect) {
1246    use crate::common::SortDirection;
1247    use crate::keymap::Mode;
1248    use ratatui::prelude::*;
1249    use ratatui::widgets::Clear;
1250
1251    frame.render_widget(Clear, area);
1252
1253    let chunks = Layout::default()
1254        .direction(Direction::Vertical)
1255        .constraints([
1256            Constraint::Length(3), // Filter
1257            Constraint::Min(0),    // Table
1258        ])
1259        .split(area);
1260
1261    let filtered_count =
1262        filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter).len();
1263    let page_size = app.sqs_state.queues.page_size.value();
1264    let total_pages = filtered_count.div_ceil(page_size);
1265    let current_page = app.sqs_state.queues.selected / page_size;
1266    let pagination = crate::ui::render_pagination_text(current_page, total_pages);
1267
1268    render_simple_filter(
1269        frame,
1270        chunks[0],
1271        SimpleFilterConfig {
1272            filter_text: &app.sqs_state.queues.filter,
1273            placeholder: "Search by queue name prefix",
1274            pagination: &pagination,
1275            mode: app.mode,
1276            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1277            is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1278        },
1279    );
1280
1281    let filtered: Vec<_> =
1282        filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter);
1283
1284    let start_idx = current_page * page_size;
1285    let end_idx = (start_idx + page_size).min(filtered.len());
1286    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1287
1288    let title = format!(" Queues ({}) ", filtered.len());
1289
1290    let columns: Vec<Box<dyn crate::ui::table::Column<Queue>>> = app
1291        .sqs_visible_column_ids
1292        .iter()
1293        .filter_map(|col_id| {
1294            SqsColumn::from_id(col_id)
1295                .map(|col| Box::new(col) as Box<dyn crate::ui::table::Column<Queue>>)
1296        })
1297        .collect();
1298
1299    let expanded_index = app.sqs_state.queues.expanded_item.and_then(|idx| {
1300        if idx >= start_idx && idx < end_idx {
1301            Some(idx - start_idx)
1302        } else {
1303            None
1304        }
1305    });
1306
1307    let config = crate::ui::table::TableConfig {
1308        items: paginated,
1309        selected_index: app.sqs_state.queues.selected % page_size,
1310        expanded_index,
1311        columns: &columns,
1312        sort_column: "Name",
1313        sort_direction: SortDirection::Asc,
1314        title,
1315        area: chunks[1],
1316        get_expanded_content: Some(Box::new(|queue: &Queue| {
1317            crate::ui::table::expanded_from_columns(&columns, queue)
1318        })),
1319        is_active: app.mode != Mode::FilterInput,
1320    };
1321
1322    crate::ui::table::render_table(frame, config);
1323}
1324
1325#[cfg(test)]
1326mod tests {
1327    use super::*;
1328    use crate::common::CyclicEnum;
1329
1330    #[test]
1331    fn test_sqs_state_initialization() {
1332        let state = State::new();
1333        assert_eq!(state.queues.items.len(), 0);
1334        assert_eq!(state.queues.selected, 0);
1335        assert_eq!(state.queues.filter, "");
1336        assert_eq!(state.queues.page_size.value(), 50);
1337        assert_eq!(state.input_focus, InputFocus::Filter);
1338    }
1339
1340    #[test]
1341    fn test_filtered_queues_empty_filter() {
1342        let queues = vec![
1343            Queue {
1344                name: "queue1".to_string(),
1345                url: String::new(),
1346                queue_type: "Standard".to_string(),
1347                created_timestamp: String::new(),
1348                messages_available: "0".to_string(),
1349                messages_in_flight: "0".to_string(),
1350                encryption: "Disabled".to_string(),
1351                content_based_deduplication: "Disabled".to_string(),
1352                last_modified_timestamp: String::new(),
1353                visibility_timeout: String::new(),
1354                message_retention_period: String::new(),
1355                maximum_message_size: String::new(),
1356                delivery_delay: String::new(),
1357                receive_message_wait_time: String::new(),
1358                high_throughput_fifo: "N/A".to_string(),
1359                deduplication_scope: "N/A".to_string(),
1360                fifo_throughput_limit: "N/A".to_string(),
1361                dead_letter_queue: "-".to_string(),
1362                messages_delayed: "0".to_string(),
1363                redrive_allow_policy: "-".to_string(),
1364                redrive_policy: "".to_string(),
1365                redrive_task_id: "-".to_string(),
1366                redrive_task_start_time: "-".to_string(),
1367                redrive_task_status: "-".to_string(),
1368                redrive_task_percent: "-".to_string(),
1369                redrive_task_destination: "-".to_string(),
1370            },
1371            Queue {
1372                name: "queue2".to_string(),
1373                url: String::new(),
1374                queue_type: "Standard".to_string(),
1375                created_timestamp: String::new(),
1376                messages_available: "0".to_string(),
1377                messages_in_flight: "0".to_string(),
1378                encryption: "Disabled".to_string(),
1379                content_based_deduplication: "Disabled".to_string(),
1380                last_modified_timestamp: String::new(),
1381                visibility_timeout: String::new(),
1382                message_retention_period: String::new(),
1383                maximum_message_size: String::new(),
1384                delivery_delay: String::new(),
1385                receive_message_wait_time: String::new(),
1386                high_throughput_fifo: "N/A".to_string(),
1387                deduplication_scope: "N/A".to_string(),
1388                fifo_throughput_limit: "N/A".to_string(),
1389                dead_letter_queue: "-".to_string(),
1390                messages_delayed: "0".to_string(),
1391                redrive_allow_policy: "-".to_string(),
1392                redrive_policy: "".to_string(),
1393                redrive_task_id: "-".to_string(),
1394                redrive_task_start_time: "-".to_string(),
1395                redrive_task_status: "-".to_string(),
1396                redrive_task_percent: "-".to_string(),
1397                redrive_task_destination: "-".to_string(),
1398            },
1399        ];
1400
1401        let filtered = filtered_queues(&queues, "");
1402        assert_eq!(filtered.len(), 2);
1403    }
1404
1405    #[test]
1406    fn test_filtered_queues_with_prefix() {
1407        let queues = vec![
1408            Queue {
1409                name: "prod-orders".to_string(),
1410                url: String::new(),
1411                queue_type: "Standard".to_string(),
1412                created_timestamp: String::new(),
1413                messages_available: "0".to_string(),
1414                messages_in_flight: "0".to_string(),
1415                encryption: "Disabled".to_string(),
1416                content_based_deduplication: "Disabled".to_string(),
1417                last_modified_timestamp: String::new(),
1418                visibility_timeout: String::new(),
1419                message_retention_period: String::new(),
1420                maximum_message_size: String::new(),
1421                delivery_delay: String::new(),
1422                receive_message_wait_time: String::new(),
1423                high_throughput_fifo: "N/A".to_string(),
1424                deduplication_scope: "N/A".to_string(),
1425                fifo_throughput_limit: "N/A".to_string(),
1426                dead_letter_queue: "-".to_string(),
1427                messages_delayed: "0".to_string(),
1428                redrive_allow_policy: "-".to_string(),
1429                redrive_policy: "".to_string(),
1430                redrive_task_id: "-".to_string(),
1431                redrive_task_start_time: "-".to_string(),
1432                redrive_task_status: "-".to_string(),
1433                redrive_task_percent: "-".to_string(),
1434                redrive_task_destination: "-".to_string(),
1435            },
1436            Queue {
1437                name: "dev-orders".to_string(),
1438                url: String::new(),
1439                queue_type: "Standard".to_string(),
1440                created_timestamp: String::new(),
1441                messages_available: "0".to_string(),
1442                messages_in_flight: "0".to_string(),
1443                encryption: "Disabled".to_string(),
1444                content_based_deduplication: "Disabled".to_string(),
1445                last_modified_timestamp: String::new(),
1446                visibility_timeout: String::new(),
1447                message_retention_period: String::new(),
1448                maximum_message_size: String::new(),
1449                delivery_delay: String::new(),
1450                receive_message_wait_time: String::new(),
1451                high_throughput_fifo: "N/A".to_string(),
1452                deduplication_scope: "N/A".to_string(),
1453                fifo_throughput_limit: "N/A".to_string(),
1454                dead_letter_queue: "-".to_string(),
1455                messages_delayed: "0".to_string(),
1456                redrive_allow_policy: "-".to_string(),
1457                redrive_policy: "".to_string(),
1458                redrive_task_id: "-".to_string(),
1459                redrive_task_start_time: "-".to_string(),
1460                redrive_task_status: "-".to_string(),
1461                redrive_task_percent: "-".to_string(),
1462                redrive_task_destination: "-".to_string(),
1463            },
1464        ];
1465
1466        let filtered = filtered_queues(&queues, "prod");
1467        assert_eq!(filtered.len(), 1);
1468        assert_eq!(filtered[0].name, "prod-orders");
1469    }
1470
1471    #[test]
1472    fn test_filtered_queues_case_insensitive() {
1473        let queues = vec![Queue {
1474            name: "MyQueue".to_string(),
1475            url: String::new(),
1476            queue_type: "Standard".to_string(),
1477            created_timestamp: String::new(),
1478            messages_available: "0".to_string(),
1479            messages_in_flight: "0".to_string(),
1480            encryption: "Disabled".to_string(),
1481            content_based_deduplication: "Disabled".to_string(),
1482            last_modified_timestamp: String::new(),
1483            visibility_timeout: String::new(),
1484            message_retention_period: String::new(),
1485            maximum_message_size: String::new(),
1486            delivery_delay: String::new(),
1487            receive_message_wait_time: String::new(),
1488            high_throughput_fifo: "N/A".to_string(),
1489            deduplication_scope: "N/A".to_string(),
1490            fifo_throughput_limit: "N/A".to_string(),
1491            dead_letter_queue: "-".to_string(),
1492            messages_delayed: "0".to_string(),
1493            redrive_allow_policy: "-".to_string(),
1494            redrive_policy: "".to_string(),
1495            redrive_task_id: "-".to_string(),
1496            redrive_task_start_time: "-".to_string(),
1497            redrive_task_status: "-".to_string(),
1498            redrive_task_percent: "-".to_string(),
1499            redrive_task_destination: "-".to_string(),
1500        }];
1501
1502        let filtered = filtered_queues(&queues, "my");
1503        assert_eq!(filtered.len(), 1);
1504
1505        let filtered = filtered_queues(&queues, "MY");
1506        assert_eq!(filtered.len(), 1);
1507    }
1508
1509    #[test]
1510    fn test_pagination_page_size() {
1511        let state = State::new();
1512        assert_eq!(state.queues.page_size.value(), 50);
1513    }
1514
1515    #[test]
1516    fn test_state_initialization_with_policy() {
1517        let state = State::new();
1518        assert_eq!(state.policy_scroll, 0);
1519        assert_eq!(state.current_queue, None);
1520        assert!(state.policy_document.contains("Version"));
1521        assert!(state.policy_document.contains("2012-10-17"));
1522    }
1523
1524    #[test]
1525    fn test_extract_region() {
1526        let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
1527        assert_eq!(extract_region(url), "us-east-1");
1528
1529        let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
1530        assert_eq!(extract_region(url2), "eu-west-2");
1531    }
1532
1533    #[test]
1534    fn test_extract_account_id() {
1535        let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
1536        assert_eq!(extract_account_id(url), "123456789012");
1537
1538        let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
1539        assert_eq!(extract_account_id(url2), "987654321098");
1540    }
1541
1542    #[test]
1543    fn test_timestamp_column_width() {
1544        use crate::sqs::queue::Column;
1545        use crate::ui::table::Column as TableColumn;
1546        // Timestamps are 27 characters: "YYYY-MM-DD HH:MM:SS (UTC)"
1547        assert!(Column::Created.width() >= 27);
1548        assert!(Column::LastUpdated.width() >= 27);
1549    }
1550
1551    #[test]
1552    fn test_message_retention_period_formatting() {
1553        // Test that 345600 seconds formats to days
1554        let seconds = 345600;
1555        let formatted = crate::common::format_duration_seconds(seconds);
1556        // 345600 seconds = 4 days
1557        assert_eq!(formatted, "4d");
1558    }
1559
1560    #[test]
1561    fn test_queue_detail_tab_navigation() {
1562        let tab = QueueDetailTab::QueuePolicies;
1563        assert_eq!(tab.next(), QueueDetailTab::Monitoring);
1564        assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1565
1566        let tab = QueueDetailTab::Monitoring;
1567        assert_eq!(tab.next(), QueueDetailTab::SnsSubscriptions);
1568        assert_eq!(tab.prev(), QueueDetailTab::QueuePolicies);
1569
1570        let tab = QueueDetailTab::SnsSubscriptions;
1571        assert_eq!(tab.next(), QueueDetailTab::LambdaTriggers);
1572        assert_eq!(tab.prev(), QueueDetailTab::Monitoring);
1573
1574        let tab = QueueDetailTab::LambdaTriggers;
1575        assert_eq!(tab.next(), QueueDetailTab::EventBridgePipes);
1576        assert_eq!(tab.prev(), QueueDetailTab::SnsSubscriptions);
1577
1578        let tab = QueueDetailTab::EventBridgePipes;
1579        assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueue);
1580        assert_eq!(tab.prev(), QueueDetailTab::LambdaTriggers);
1581
1582        let tab = QueueDetailTab::DeadLetterQueue;
1583        assert_eq!(tab.next(), QueueDetailTab::Tagging);
1584        assert_eq!(tab.prev(), QueueDetailTab::EventBridgePipes);
1585
1586        let tab = QueueDetailTab::Tagging;
1587        assert_eq!(tab.next(), QueueDetailTab::Encryption);
1588        assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueue);
1589
1590        let tab = QueueDetailTab::Encryption;
1591        assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1592        assert_eq!(tab.prev(), QueueDetailTab::Tagging);
1593
1594        let tab = QueueDetailTab::DeadLetterQueueRedriveTasks;
1595        assert_eq!(tab.next(), QueueDetailTab::QueuePolicies);
1596        assert_eq!(tab.prev(), QueueDetailTab::Encryption);
1597
1598        let tab = QueueDetailTab::QueuePolicies;
1599        assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1600    }
1601
1602    #[test]
1603    fn test_queue_detail_tab_all() {
1604        let tabs = QueueDetailTab::all();
1605        assert_eq!(tabs.len(), 9);
1606        assert_eq!(tabs[0], QueueDetailTab::QueuePolicies);
1607        assert_eq!(tabs[1], QueueDetailTab::Monitoring);
1608        assert_eq!(tabs[2], QueueDetailTab::SnsSubscriptions);
1609        assert_eq!(tabs[3], QueueDetailTab::LambdaTriggers);
1610        assert_eq!(tabs[4], QueueDetailTab::EventBridgePipes);
1611        assert_eq!(tabs[5], QueueDetailTab::DeadLetterQueue);
1612        assert_eq!(tabs[6], QueueDetailTab::Tagging);
1613        assert_eq!(tabs[7], QueueDetailTab::Encryption);
1614        assert_eq!(tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
1615    }
1616
1617    #[test]
1618    fn test_queue_detail_tab_names() {
1619        assert_eq!(QueueDetailTab::QueuePolicies.name(), "Queue policies");
1620        assert_eq!(QueueDetailTab::SnsSubscriptions.name(), "SNS subscriptions");
1621        assert_eq!(QueueDetailTab::LambdaTriggers.name(), "Lambda triggers");
1622        assert_eq!(QueueDetailTab::EventBridgePipes.name(), "EventBridge Pipes");
1623        assert_eq!(QueueDetailTab::Tagging.name(), "Tagging");
1624        assert_eq!(QueueDetailTab::DeadLetterQueue.name(), "Dead-letter queue");
1625    }
1626
1627    #[test]
1628    fn test_trigger_column_all() {
1629        use crate::sqs::trigger::Column as TriggerColumn;
1630        assert_eq!(TriggerColumn::all().len(), 4);
1631    }
1632
1633    #[test]
1634    fn test_trigger_column_ids() {
1635        use crate::sqs::trigger::Column as TriggerColumn;
1636        let ids = TriggerColumn::ids();
1637        assert_eq!(ids.len(), 4);
1638        assert!(ids.contains(&"column.sqs.trigger.uuid"));
1639        assert!(ids.contains(&"column.sqs.trigger.arn"));
1640        assert!(ids.contains(&"column.sqs.trigger.status"));
1641        assert!(ids.contains(&"column.sqs.trigger.last_modified"));
1642    }
1643
1644    #[test]
1645    fn test_trigger_column_from_id() {
1646        use crate::sqs::trigger::Column as TriggerColumn;
1647        assert_eq!(
1648            TriggerColumn::from_id("column.sqs.trigger.uuid"),
1649            Some(TriggerColumn::Uuid)
1650        );
1651        assert_eq!(
1652            TriggerColumn::from_id("column.sqs.trigger.arn"),
1653            Some(TriggerColumn::Arn)
1654        );
1655        assert_eq!(
1656            TriggerColumn::from_id("column.sqs.trigger.status"),
1657            Some(TriggerColumn::Status)
1658        );
1659        assert_eq!(
1660            TriggerColumn::from_id("column.sqs.trigger.last_modified"),
1661            Some(TriggerColumn::LastModified)
1662        );
1663        assert_eq!(TriggerColumn::from_id("invalid"), None);
1664    }
1665
1666    #[test]
1667    fn test_trigger_status_rendering() {
1668        use crate::sqs::trigger::{Column as TriggerColumn, LambdaTrigger};
1669        use crate::ui::table::Column;
1670
1671        let trigger = LambdaTrigger {
1672            uuid: "test-uuid".to_string(),
1673            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
1674            status: "Enabled".to_string(),
1675            last_modified: "1609459200".to_string(),
1676        };
1677
1678        let (text, style) = TriggerColumn::Status.render(&trigger);
1679        assert_eq!(text, "✅ Enabled");
1680        assert_eq!(style.fg, Some(ratatui::style::Color::Green));
1681    }
1682
1683    #[test]
1684    fn test_trigger_timestamp_rendering() {
1685        use crate::sqs::trigger::{Column as TriggerColumn, LambdaTrigger};
1686        use crate::ui::table::Column;
1687
1688        let trigger = LambdaTrigger {
1689            uuid: "test-uuid".to_string(),
1690            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
1691            status: "Enabled".to_string(),
1692            last_modified: "1609459200".to_string(),
1693        };
1694
1695        let (text, _) = TriggerColumn::LastModified.render(&trigger);
1696        assert!(text.contains("2021-01-01"));
1697        assert!(text.contains("(UTC)"));
1698    }
1699
1700    #[test]
1701    fn test_state_initializes_trigger_columns() {
1702        let state = State::new();
1703        assert_eq!(state.trigger_column_ids.len(), 4);
1704        assert_eq!(state.trigger_visible_column_ids.len(), 4);
1705        assert_eq!(state.trigger_column_ids, state.trigger_visible_column_ids);
1706    }
1707
1708    #[test]
1709    fn test_trigger_state_has_filter() {
1710        let mut state = State::new();
1711        state.detail_tab = QueueDetailTab::LambdaTriggers;
1712        state.triggers.filter = "test-filter".to_string();
1713
1714        // Verify filter is set
1715        assert_eq!(state.triggers.filter, "test-filter");
1716        assert_eq!(state.detail_tab, QueueDetailTab::LambdaTriggers);
1717    }
1718
1719    #[test]
1720    fn test_trigger_filtering() {
1721        use crate::sqs::trigger::LambdaTrigger;
1722
1723        let triggers = [
1724            LambdaTrigger {
1725                uuid: "uuid-123".to_string(),
1726                arn: "arn:aws:lambda:us-east-1:123:function:test1".to_string(),
1727                status: "Enabled".to_string(),
1728                last_modified: "1609459200".to_string(),
1729            },
1730            LambdaTrigger {
1731                uuid: "uuid-456".to_string(),
1732                arn: "arn:aws:lambda:us-east-1:123:function:test2".to_string(),
1733                status: "Enabled".to_string(),
1734                last_modified: "1609459200".to_string(),
1735            },
1736        ];
1737
1738        // Filter by UUID
1739        let filtered: Vec<_> = triggers.iter().filter(|t| t.uuid.contains("123")).collect();
1740        assert_eq!(filtered.len(), 1);
1741        assert_eq!(filtered[0].uuid, "uuid-123");
1742
1743        // Filter by ARN
1744        let filtered: Vec<_> = triggers
1745            .iter()
1746            .filter(|t| t.arn.contains("test2"))
1747            .collect();
1748        assert_eq!(filtered.len(), 1);
1749        assert_eq!(
1750            filtered[0].arn,
1751            "arn:aws:lambda:us-east-1:123:function:test2"
1752        );
1753    }
1754
1755    #[test]
1756    fn test_trigger_pagination() {
1757        let mut state = State::new();
1758        state.triggers.items = (0..10)
1759            .map(|i| crate::sqs::LambdaTrigger {
1760                uuid: format!("uuid-{}", i),
1761                arn: format!("arn:aws:lambda:us-east-1:123:function:test{}", i),
1762                status: "Enabled".to_string(),
1763                last_modified: "1609459200".to_string(),
1764            })
1765            .collect();
1766
1767        assert_eq!(state.triggers.items.len(), 10);
1768        assert_eq!(state.triggers.page_size.value(), 50); // Default page size
1769    }
1770
1771    #[test]
1772    fn test_trigger_column_visibility() {
1773        let mut state = State::new();
1774
1775        // All columns visible by default
1776        assert_eq!(state.trigger_visible_column_ids.len(), 4);
1777
1778        // Remove a column
1779        state.trigger_visible_column_ids.remove(0);
1780        assert_eq!(state.trigger_visible_column_ids.len(), 3);
1781
1782        // Add it back
1783        state
1784            .trigger_visible_column_ids
1785            .push(state.trigger_column_ids[0].clone());
1786        assert_eq!(state.trigger_visible_column_ids.len(), 4);
1787    }
1788
1789    #[test]
1790    fn test_trigger_page_size_options() {
1791        use crate::common::PageSize;
1792        let mut state = State::new();
1793
1794        // Default is 50
1795        assert_eq!(state.triggers.page_size, PageSize::Fifty);
1796
1797        // Can change to other sizes
1798        state.triggers.page_size = PageSize::Ten;
1799        assert_eq!(state.triggers.page_size.value(), 10);
1800
1801        state.triggers.page_size = PageSize::TwentyFive;
1802        assert_eq!(state.triggers.page_size.value(), 25);
1803
1804        state.triggers.page_size = PageSize::OneHundred;
1805        assert_eq!(state.triggers.page_size.value(), 100);
1806    }
1807
1808    #[test]
1809    fn test_trigger_loading_state() {
1810        let mut state = State::new();
1811
1812        // Initially not loading
1813        assert!(!state.triggers.loading);
1814
1815        // Can set to loading
1816        state.triggers.loading = true;
1817        assert!(state.triggers.loading);
1818
1819        // Can clear loading
1820        state.triggers.loading = false;
1821        assert!(!state.triggers.loading);
1822    }
1823
1824    #[test]
1825    fn test_trigger_sort_by_last_modified() {
1826        let mut triggers = [
1827            crate::sqs::LambdaTrigger {
1828                uuid: "uuid-2".to_string(),
1829                arn: "arn2".to_string(),
1830                status: "Enabled".to_string(),
1831                last_modified: "1609459300".to_string(), // Later
1832            },
1833            crate::sqs::LambdaTrigger {
1834                uuid: "uuid-1".to_string(),
1835                arn: "arn1".to_string(),
1836                status: "Enabled".to_string(),
1837                last_modified: "1609459200".to_string(), // Earlier
1838            },
1839        ];
1840
1841        // Sort ascending (oldest first)
1842        triggers.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
1843
1844        assert_eq!(triggers[0].uuid, "uuid-1");
1845        assert_eq!(triggers[1].uuid, "uuid-2");
1846    }
1847
1848    #[test]
1849    fn test_trigger_pagination_calculation() {
1850        use crate::common::PageSize;
1851        let mut state = State::new();
1852
1853        // Add 25 triggers
1854        state.triggers.items = (0..25)
1855            .map(|i| crate::sqs::LambdaTrigger {
1856                uuid: format!("uuid-{}", i),
1857                arn: format!("arn{}", i),
1858                status: "Enabled".to_string(),
1859                last_modified: "1609459200".to_string(),
1860            })
1861            .collect();
1862
1863        // With page size 10, should have 3 pages
1864        state.triggers.page_size = PageSize::Ten;
1865        let page_size = state.triggers.page_size.value();
1866        let total_pages = state.triggers.items.len().div_ceil(page_size);
1867        assert_eq!(total_pages, 3);
1868
1869        // Page 0: items 0-9
1870        let current_page = 0;
1871        let start_idx = current_page * page_size;
1872        let end_idx = (start_idx + page_size).min(state.triggers.items.len());
1873        assert_eq!(start_idx, 0);
1874        assert_eq!(end_idx, 10);
1875
1876        // Page 2: items 20-24
1877        let current_page = 2;
1878        let start_idx = current_page * page_size;
1879        let end_idx = (start_idx + page_size).min(state.triggers.items.len());
1880        assert_eq!(start_idx, 20);
1881        assert_eq!(end_idx, 25);
1882    }
1883
1884    #[test]
1885    fn test_monitoring_metric_data_with_values() {
1886        let mut state = State::new();
1887        // Mock obfuscated metric data
1888        state.metric_data = vec![
1889            (1700000000, 0.0),
1890            (1700000060, 5.0),
1891            (1700000120, 10.0),
1892            (1700000180, 0.0),
1893        ];
1894        assert_eq!(state.metric_data.len(), 4);
1895        assert_eq!(state.metric_data[0], (1700000000, 0.0));
1896        assert_eq!(state.metric_data[1], (1700000060, 5.0));
1897    }
1898
1899    #[test]
1900    fn test_monitoring_all_metrics_initialized() {
1901        let state = State::new();
1902        assert!(state.metric_data.is_empty());
1903        assert!(state.metric_data_delayed.is_empty());
1904        assert!(state.metric_data_not_visible.is_empty());
1905        assert!(state.metric_data_visible.is_empty());
1906        assert!(state.metric_data_empty_receives.is_empty());
1907        assert!(state.metric_data_messages_deleted.is_empty());
1908        assert!(state.metric_data_messages_received.is_empty());
1909        assert!(state.metric_data_messages_sent.is_empty());
1910        assert!(state.metric_data_sent_message_size.is_empty());
1911        assert_eq!(state.monitoring_scroll, 0);
1912    }
1913
1914    #[test]
1915    fn test_monitoring_scroll_pages() {
1916        let mut state = State::new();
1917        assert_eq!(state.monitoring_scroll, 0);
1918
1919        // Scroll to page 1
1920        state.monitoring_scroll = 1;
1921        assert_eq!(state.monitoring_scroll, 1);
1922
1923        // Scroll to page 2
1924        state.monitoring_scroll = 2;
1925        assert_eq!(state.monitoring_scroll, 2);
1926    }
1927
1928    #[test]
1929    fn test_monitoring_delayed_metrics() {
1930        let mut state = State::new();
1931        state.metric_data_delayed = vec![(1700000000, 1.0), (1700000060, 2.0)];
1932        assert_eq!(state.metric_data_delayed.len(), 2);
1933        assert_eq!(state.metric_data_delayed[0].1, 1.0);
1934    }
1935
1936    #[test]
1937    fn test_monitoring_not_visible_metrics() {
1938        let mut state = State::new();
1939        state.metric_data_not_visible = vec![(1700000000, 3.0), (1700000060, 4.0)];
1940        assert_eq!(state.metric_data_not_visible.len(), 2);
1941        assert_eq!(state.metric_data_not_visible[1].1, 4.0);
1942    }
1943
1944    #[test]
1945    fn test_monitoring_visible_metrics() {
1946        let mut state = State::new();
1947        state.metric_data_visible = vec![(1700000000, 5.0), (1700000060, 6.0)];
1948        assert_eq!(state.metric_data_visible.len(), 2);
1949        assert_eq!(state.metric_data_visible[0].1, 5.0);
1950    }
1951
1952    #[test]
1953    fn test_monitoring_empty_receives_metrics() {
1954        let mut state = State::new();
1955        state.metric_data_empty_receives = vec![(1700000000, 10.0), (1700000060, 15.0)];
1956        assert_eq!(state.metric_data_empty_receives.len(), 2);
1957        assert_eq!(state.metric_data_empty_receives[0].1, 10.0);
1958    }
1959
1960    #[test]
1961    fn test_monitoring_messages_deleted_metrics() {
1962        let mut state = State::new();
1963        state.metric_data_messages_deleted = vec![(1700000000, 20.0), (1700000060, 25.0)];
1964        assert_eq!(state.metric_data_messages_deleted.len(), 2);
1965        assert_eq!(state.metric_data_messages_deleted[0].1, 20.0);
1966    }
1967
1968    #[test]
1969    fn test_monitoring_messages_received_metrics() {
1970        let mut state = State::new();
1971        state.metric_data_messages_received = vec![(1700000000, 30.0), (1700000060, 35.0)];
1972        assert_eq!(state.metric_data_messages_received.len(), 2);
1973        assert_eq!(state.metric_data_messages_received[0].1, 30.0);
1974    }
1975
1976    #[test]
1977    fn test_monitoring_messages_sent_metrics() {
1978        let mut state = State::new();
1979        state.metric_data_messages_sent = vec![(1700000000, 40.0), (1700000060, 45.0)];
1980        assert_eq!(state.metric_data_messages_sent.len(), 2);
1981        assert_eq!(state.metric_data_messages_sent[0].1, 40.0);
1982    }
1983
1984    #[test]
1985    fn test_monitoring_sent_message_size_metrics() {
1986        let mut state = State::new();
1987        state.metric_data_sent_message_size = vec![(1700000000, 1024.0), (1700000060, 2048.0)];
1988        assert_eq!(state.metric_data_sent_message_size.len(), 2);
1989        assert_eq!(state.metric_data_sent_message_size[0].1, 1024.0);
1990    }
1991
1992    #[test]
1993    fn test_trigger_expand_collapse() {
1994        let mut state = State::new();
1995
1996        // Initially no item expanded
1997        assert_eq!(state.triggers.expanded_item, None);
1998
1999        // Expand item 0
2000        state.triggers.expanded_item = Some(0);
2001        assert_eq!(state.triggers.expanded_item, Some(0));
2002
2003        // Collapse (set to None)
2004        state.triggers.expanded_item = None;
2005        assert_eq!(state.triggers.expanded_item, None);
2006    }
2007
2008    #[test]
2009    fn test_trigger_filter_visibility() {
2010        let mut state = State::new();
2011
2012        // Filter starts empty
2013        assert!(state.triggers.filter.is_empty());
2014
2015        // Can set filter
2016        state.triggers.filter = "test".to_string();
2017        assert_eq!(state.triggers.filter, "test");
2018
2019        // Can clear filter
2020        state.triggers.filter.clear();
2021        assert!(state.triggers.filter.is_empty());
2022    }
2023
2024    #[test]
2025    fn test_pipe_column_ids_have_correct_prefix() {
2026        for col in PipeColumn::all() {
2027            assert!(
2028                col.id().starts_with("column.sqs.pipe."),
2029                "PipeColumn ID '{}' should start with 'column.sqs.pipe.'",
2030                col.id()
2031            );
2032        }
2033    }
2034
2035    #[test]
2036    fn test_tags_sorted_by_value() {
2037        let mut state = State::new();
2038        state.tags.items = vec![
2039            QueueTag {
2040                key: "env".to_string(),
2041                value: "prod".to_string(),
2042            },
2043            QueueTag {
2044                key: "team".to_string(),
2045                value: "backend".to_string(),
2046            },
2047            QueueTag {
2048                key: "app".to_string(),
2049                value: "api".to_string(),
2050            },
2051        ];
2052
2053        let mut sorted = state.tags.items.clone();
2054        sorted.sort_by(|a, b| a.value.cmp(&b.value));
2055
2056        assert_eq!(sorted[0].value, "api");
2057        assert_eq!(sorted[1].value, "backend");
2058        assert_eq!(sorted[2].value, "prod");
2059    }
2060
2061    #[test]
2062    fn test_tags_initialization() {
2063        let state = State::new();
2064        assert_eq!(state.tags.items.len(), 0);
2065        assert_eq!(state.tag_column_ids.len(), 2);
2066        assert_eq!(state.tag_visible_column_ids.len(), 2);
2067    }
2068
2069    #[test]
2070    fn test_queue_tag_structure() {
2071        let tag = QueueTag {
2072            key: "Environment".to_string(),
2073            value: "Production".to_string(),
2074        };
2075        assert_eq!(tag.key, "Environment");
2076        assert_eq!(tag.value, "Production");
2077    }
2078
2079    #[test]
2080    fn test_tags_table_state() {
2081        let mut state = State::new();
2082        state.tags.items = vec![
2083            QueueTag {
2084                key: "Env".to_string(),
2085                value: "prod".to_string(),
2086            },
2087            QueueTag {
2088                key: "Team".to_string(),
2089                value: "backend".to_string(),
2090            },
2091        ];
2092        assert_eq!(state.tags.items.len(), 2);
2093        assert_eq!(state.tags.selected, 0);
2094        assert_eq!(state.tags.filter, "");
2095    }
2096
2097    #[test]
2098    fn test_tags_filtering() {
2099        let tags = [
2100            QueueTag {
2101                key: "Environment".to_string(),
2102                value: "production".to_string(),
2103            },
2104            QueueTag {
2105                key: "Team".to_string(),
2106                value: "backend".to_string(),
2107            },
2108            QueueTag {
2109                key: "Project".to_string(),
2110                value: "api".to_string(),
2111            },
2112        ];
2113
2114        // Test filtering by key
2115        let filtered: Vec<_> = tags
2116            .iter()
2117            .filter(|t| t.key.to_lowercase().contains("env"))
2118            .collect();
2119        assert_eq!(filtered.len(), 1);
2120        assert_eq!(filtered[0].key, "Environment");
2121
2122        // Test filtering by value
2123        let filtered: Vec<_> = tags
2124            .iter()
2125            .filter(|t| t.value.to_lowercase().contains("back"))
2126            .collect();
2127        assert_eq!(filtered.len(), 1);
2128        assert_eq!(filtered[0].value, "backend");
2129    }
2130
2131    #[test]
2132    fn test_tags_column_ids() {
2133        use crate::sqs::tag::Column as TagColumn;
2134        let ids = TagColumn::ids();
2135        assert_eq!(ids.len(), 2);
2136        assert_eq!(ids[0], "column.sqs.tag.key");
2137        assert_eq!(ids[1], "column.sqs.tag.value");
2138    }
2139
2140    #[test]
2141    fn test_tags_column_from_id() {
2142        use crate::sqs::tag::Column as TagColumn;
2143        assert!(TagColumn::from_id("column.sqs.tag.key").is_some());
2144        assert!(TagColumn::from_id("column.sqs.tag.value").is_some());
2145        assert!(TagColumn::from_id("invalid").is_none());
2146    }
2147
2148    #[test]
2149    fn test_subscriptions_initialization() {
2150        let state = State::new();
2151        assert_eq!(state.subscriptions.items.len(), 0);
2152        assert_eq!(state.subscription_column_ids.len(), 2);
2153        assert_eq!(state.subscription_visible_column_ids.len(), 2);
2154        assert_eq!(state.subscription_region_filter, "");
2155    }
2156
2157    #[test]
2158    fn test_subscription_column_ids() {
2159        use crate::sqs::sub::Column as SubscriptionColumn;
2160        let ids = SubscriptionColumn::ids();
2161        assert_eq!(ids.len(), 2);
2162        assert_eq!(ids[0], "column.sqs.subscription.subscription_arn");
2163        assert_eq!(ids[1], "column.sqs.subscription.topic_arn");
2164    }
2165
2166    #[test]
2167    fn test_subscription_column_from_id() {
2168        use crate::sqs::sub::Column as SubscriptionColumn;
2169        assert!(SubscriptionColumn::from_id("column.sqs.subscription.subscription_arn").is_some());
2170        assert!(SubscriptionColumn::from_id("column.sqs.subscription.topic_arn").is_some());
2171        assert!(SubscriptionColumn::from_id("invalid").is_none());
2172    }
2173
2174    #[test]
2175    fn test_subscription_region_filter_default() {
2176        let state = State::new();
2177        // Default is empty string, which means use current region
2178        assert_eq!(state.subscription_region_filter, "");
2179    }
2180
2181    #[test]
2182    fn test_subscription_region_filter_display() {
2183        let mut state = State::new();
2184
2185        // When empty, should show current region
2186        assert_eq!(state.subscription_region_filter, "");
2187
2188        // When set, should show selected region
2189        state.subscription_region_filter = "us-west-2".to_string();
2190        assert_eq!(state.subscription_region_filter, "us-west-2");
2191    }
2192
2193    #[test]
2194    fn test_subscription_region_selected_index() {
2195        let state = State::new();
2196        assert_eq!(state.subscription_region_selected, 0);
2197    }
2198
2199    #[test]
2200    fn test_encryption_tab_in_all() {
2201        let tabs = QueueDetailTab::all();
2202        assert!(tabs.contains(&QueueDetailTab::Encryption));
2203    }
2204
2205    #[test]
2206    fn test_encryption_tab_name() {
2207        assert_eq!(QueueDetailTab::Encryption.name(), "Encryption");
2208    }
2209
2210    #[test]
2211    fn test_encryption_tab_order() {
2212        let tabs = QueueDetailTab::all();
2213        let dlq_idx = tabs
2214            .iter()
2215            .position(|t| *t == QueueDetailTab::DeadLetterQueue)
2216            .unwrap();
2217        let tagging_idx = tabs
2218            .iter()
2219            .position(|t| *t == QueueDetailTab::Tagging)
2220            .unwrap();
2221        let encryption_idx = tabs
2222            .iter()
2223            .position(|t| *t == QueueDetailTab::Encryption)
2224            .unwrap();
2225
2226        // Encryption should be after Tagging and DeadLetterQueue should be before Tagging
2227        assert!(dlq_idx < tagging_idx);
2228        assert!(encryption_idx > tagging_idx);
2229    }
2230
2231    #[test]
2232    fn test_dlq_redrive_tasks_tab_in_all() {
2233        let tabs = QueueDetailTab::all();
2234        assert!(tabs.contains(&QueueDetailTab::DeadLetterQueueRedriveTasks));
2235    }
2236
2237    #[test]
2238    fn test_dlq_redrive_tasks_tab_name() {
2239        assert_eq!(
2240            QueueDetailTab::DeadLetterQueueRedriveTasks.name(),
2241            "Dead-letter queue redrive tasks"
2242        );
2243    }
2244
2245    #[test]
2246    fn test_dlq_redrive_tasks_tab_order() {
2247        let tabs = QueueDetailTab::all();
2248        let encryption_idx = tabs
2249            .iter()
2250            .position(|t| *t == QueueDetailTab::Encryption)
2251            .unwrap();
2252        let redrive_idx = tabs
2253            .iter()
2254            .position(|t| *t == QueueDetailTab::DeadLetterQueueRedriveTasks)
2255            .unwrap();
2256
2257        // DeadLetterQueueRedriveTasks should be after Encryption (last tab)
2258        assert!(redrive_idx > encryption_idx);
2259        assert_eq!(redrive_idx, tabs.len() - 1);
2260    }
2261
2262    #[test]
2263    fn test_tab_strip_matches_enum_order() {
2264        // This test ensures the hardcoded tab strip in render_queue_detail matches QueueDetailTab::all()
2265        let all_tabs = QueueDetailTab::all();
2266        assert_eq!(all_tabs.len(), 9);
2267
2268        // Verify order matches
2269        assert_eq!(all_tabs[0], QueueDetailTab::QueuePolicies);
2270        assert_eq!(all_tabs[1], QueueDetailTab::Monitoring);
2271        assert_eq!(all_tabs[2], QueueDetailTab::SnsSubscriptions);
2272        assert_eq!(all_tabs[3], QueueDetailTab::LambdaTriggers);
2273        assert_eq!(all_tabs[4], QueueDetailTab::EventBridgePipes);
2274        assert_eq!(all_tabs[5], QueueDetailTab::DeadLetterQueue);
2275        assert_eq!(all_tabs[6], QueueDetailTab::Tagging);
2276        assert_eq!(all_tabs[7], QueueDetailTab::Encryption);
2277        assert_eq!(all_tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
2278    }
2279
2280    #[test]
2281    fn test_monitoring_tab_in_all() {
2282        let all_tabs = QueueDetailTab::all();
2283        assert!(all_tabs.contains(&QueueDetailTab::Monitoring));
2284    }
2285
2286    #[test]
2287    fn test_monitoring_tab_name() {
2288        assert_eq!(QueueDetailTab::Monitoring.name(), "Monitoring");
2289    }
2290
2291    #[test]
2292    fn test_monitoring_tab_order() {
2293        let all_tabs = QueueDetailTab::all();
2294        let monitoring_index = all_tabs
2295            .iter()
2296            .position(|t| *t == QueueDetailTab::Monitoring)
2297            .unwrap();
2298        assert_eq!(monitoring_index, 1); // Should be second, after QueuePolicies
2299    }
2300}