rusticity_term/ui/
sqs.rs

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