Skip to main content

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                        ..Default::default()
525                    },
526                    MetricChart {
527                        title: "Approximate number of messages delayed",
528                        data: &app.sqs_state.metric_data_delayed,
529                        y_axis_label: "Count",
530                        x_axis_label: None,
531                        ..Default::default()
532                    },
533                    MetricChart {
534                        title: "Approximate number of messages not visible",
535                        data: &app.sqs_state.metric_data_not_visible,
536                        y_axis_label: "Count",
537                        x_axis_label: None,
538                        ..Default::default()
539                    },
540                    MetricChart {
541                        title: "Approximate number of messages visible",
542                        data: &app.sqs_state.metric_data_visible,
543                        y_axis_label: "Count",
544                        x_axis_label: None,
545                        ..Default::default()
546                    },
547                    MetricChart {
548                        title: "Number of empty receives",
549                        data: &app.sqs_state.metric_data_empty_receives,
550                        y_axis_label: "Count",
551                        x_axis_label: None,
552                        ..Default::default()
553                    },
554                    MetricChart {
555                        title: "Number of messages deleted",
556                        data: &app.sqs_state.metric_data_messages_deleted,
557                        y_axis_label: "Count",
558                        x_axis_label: None,
559                        ..Default::default()
560                    },
561                    MetricChart {
562                        title: "Number of messages received",
563                        data: &app.sqs_state.metric_data_messages_received,
564                        y_axis_label: "Count",
565                        x_axis_label: None,
566                        ..Default::default()
567                    },
568                    MetricChart {
569                        title: "Number of messages sent",
570                        data: &app.sqs_state.metric_data_messages_sent,
571                        y_axis_label: "Count",
572                        x_axis_label: None,
573                        ..Default::default()
574                    },
575                    MetricChart {
576                        title: "Sent message size",
577                        data: &app.sqs_state.metric_data_sent_message_size,
578                        y_axis_label: "Bytes",
579                        x_axis_label: None,
580                        ..Default::default()
581                    },
582                ],
583                &[],
584                &[],
585                &[],
586                app.sqs_state.monitoring_scroll,
587            );
588        }
589        QueueDetailTab::SnsSubscriptions => {
590            render_subscriptions_tab(frame, app, chunks[2]);
591        }
592        QueueDetailTab::LambdaTriggers => {
593            render_lambda_triggers_tab(frame, app, chunks[2]);
594        }
595        QueueDetailTab::EventBridgePipes => {
596            render_eventbridge_pipes_tab(frame, app, chunks[2]);
597        }
598        QueueDetailTab::DeadLetterQueue => {
599            render_dead_letter_queue_tab(frame, app, chunks[2]);
600        }
601        QueueDetailTab::Tagging => {
602            render_tags_tab(frame, app, chunks[2]);
603        }
604        QueueDetailTab::Encryption => {
605            render_encryption_tab(frame, app, chunks[2]);
606        }
607        QueueDetailTab::DeadLetterQueueRedriveTasks => {
608            render_dlq_redrive_tasks_tab(frame, app, chunks[2]);
609        }
610    }
611}
612
613fn render_details_fields(queue: &Queue) -> Vec<ratatui::text::Line<'static>> {
614    let max_msg_size = queue
615        .maximum_message_size
616        .split_whitespace()
617        .next()
618        .and_then(|s| s.parse::<i64>().ok())
619        .map(format_bytes)
620        .unwrap_or_else(|| queue.maximum_message_size.clone());
621
622    let retention_period = queue
623        .message_retention_period
624        .parse::<i32>()
625        .ok()
626        .map(format_duration_seconds)
627        .unwrap_or_else(|| queue.message_retention_period.clone());
628
629    let visibility_timeout = queue
630        .visibility_timeout
631        .parse::<i32>()
632        .ok()
633        .map(format_duration_seconds)
634        .unwrap_or_else(|| queue.visibility_timeout.clone());
635
636    let delivery_delay = queue
637        .delivery_delay
638        .parse::<i32>()
639        .ok()
640        .map(format_duration_seconds)
641        .unwrap_or_else(|| queue.delivery_delay.clone());
642
643    let receive_wait_time = queue
644        .receive_message_wait_time
645        .parse::<i32>()
646        .ok()
647        .map(format_duration_seconds)
648        .unwrap_or_else(|| queue.receive_message_wait_time.clone());
649
650    vec![
651        labeled_field("Name", &queue.name),
652        labeled_field("Type", &queue.queue_type),
653        labeled_field(
654            "ARN",
655            format!(
656                "arn:aws:sqs:{}:{}:{}",
657                extract_region(&queue.url),
658                extract_account_id(&queue.url),
659                queue.name
660            ),
661        ),
662        labeled_field("Encryption", &queue.encryption),
663        labeled_field("URL", &queue.url),
664        labeled_field("Dead-letter queue", &queue.dead_letter_queue),
665        labeled_field("Created", format_unix_timestamp(&queue.created_timestamp)),
666        labeled_field("Maximum message size", max_msg_size),
667        labeled_field(
668            "Last updated",
669            format_unix_timestamp(&queue.last_modified_timestamp),
670        ),
671        labeled_field("Message retention period", retention_period),
672        labeled_field("Default visibility timeout", visibility_timeout),
673        labeled_field("Messages available", &queue.messages_available),
674        labeled_field("Delivery delay", delivery_delay),
675        labeled_field(
676            "Messages in flight (not available to other consumers)",
677            &queue.messages_in_flight,
678        ),
679        labeled_field("Receive message wait time", receive_wait_time),
680        labeled_field("Messages delayed", &queue.messages_delayed),
681        labeled_field(
682            "Content-based deduplication",
683            &queue.content_based_deduplication,
684        ),
685        labeled_field("High throughput FIFO", &queue.high_throughput_fifo),
686        labeled_field("Deduplication scope", &queue.deduplication_scope),
687        labeled_field("FIFO throughput limit", &queue.fifo_throughput_limit),
688        labeled_field("Redrive allow policy", &queue.redrive_allow_policy),
689    ]
690}
691
692fn render_queue_policies_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
693    use ratatui::prelude::{Constraint, Direction, Layout};
694
695    let chunks = Layout::default()
696        .direction(Direction::Vertical)
697        .constraints([Constraint::Min(0)])
698        .split(area);
699
700    // Access policy JSON using common JSON renderer
701    render_json_highlighted(
702        frame,
703        chunks[0],
704        &app.sqs_state.policy_document,
705        app.sqs_state.policy_scroll,
706        "Access policy",
707        true,
708    );
709}
710
711fn render_lambda_triggers_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
712    use ratatui::prelude::*;
713
714    let chunks = Layout::default()
715        .direction(Direction::Vertical)
716        .constraints([Constraint::Length(3), Constraint::Min(0)])
717        .split(area);
718
719    let filtered = filtered_lambda_triggers(app);
720
721    let columns: Vec<Box<dyn Column<LambdaTrigger>>> = app
722        .sqs_state
723        .trigger_visible_column_ids
724        .iter()
725        .filter_map(|id| TriggerColumn::from_id(id))
726        .map(|col| Box::new(col) as Box<dyn Column<LambdaTrigger>>)
727        .collect();
728
729    // Pagination
730    let page_size = app.sqs_state.triggers.page_size.value();
731    let total_pages = filtered.len().div_ceil(page_size.max(1));
732    let current_page = app.sqs_state.triggers.selected / page_size.max(1);
733    let pagination = render_pagination_text(current_page, total_pages);
734
735    // Filter at top
736    render_simple_filter(
737        frame,
738        chunks[0],
739        SimpleFilterConfig {
740            filter_text: &app.sqs_state.triggers.filter,
741            placeholder: "Search triggers",
742            pagination: &pagination,
743            mode: app.mode,
744            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
745            is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
746        },
747    );
748
749    let start_idx = current_page * page_size;
750    let end_idx = (start_idx + page_size).min(filtered.len());
751    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
752
753    let expanded_index = app.sqs_state.triggers.expanded_item.and_then(|idx| {
754        if idx >= start_idx && idx < end_idx {
755            Some(idx - start_idx)
756        } else {
757            None
758        }
759    });
760
761    render_table(
762        frame,
763        TableConfig {
764            area: chunks[1],
765            columns: &columns,
766            items: paginated,
767            selected_index: app.sqs_state.triggers.selected % page_size.max(1),
768            is_active: app.mode != Mode::FilterInput,
769            title: format_title(&format!("Lambda triggers ({})", filtered.len())),
770            sort_column: "last_modified",
771            sort_direction: SortDirection::Asc,
772            expanded_index,
773            get_expanded_content: Some(Box::new(|trigger: &LambdaTrigger| {
774                expanded_from_columns(&columns, trigger)
775            })),
776        },
777    );
778}
779
780pub fn extract_region(url: &str) -> &str {
781    url.split("sqs.")
782        .nth(1)
783        .and_then(|s| s.split('.').next())
784        .unwrap_or("unknown")
785}
786
787pub fn extract_account_id(url: &str) -> &str {
788    url.split('/').nth(3).unwrap_or("unknown")
789}
790
791fn render_eventbridge_pipes_tab(
792    frame: &mut ratatui::Frame,
793    app: &App,
794    area: ratatui::prelude::Rect,
795) {
796    use ratatui::prelude::*;
797
798    let chunks = Layout::default()
799        .direction(Direction::Vertical)
800        .constraints([Constraint::Length(3), Constraint::Min(0)])
801        .split(area);
802
803    let filtered = filtered_eventbridge_pipes(app);
804
805    let columns: Vec<Box<dyn Column<EventBridgePipe>>> = app
806        .sqs_state
807        .pipe_visible_column_ids
808        .iter()
809        .filter_map(|id| PipeColumn::from_id(id))
810        .map(|col| Box::new(col) as Box<dyn Column<EventBridgePipe>>)
811        .collect();
812
813    let page_size = app.sqs_state.pipes.page_size.value();
814    let total_pages = filtered.len().div_ceil(page_size.max(1));
815    let current_page = app.sqs_state.pipes.selected / page_size.max(1);
816    let pagination = render_pagination_text(current_page, total_pages);
817
818    render_simple_filter(
819        frame,
820        chunks[0],
821        SimpleFilterConfig {
822            filter_text: &app.sqs_state.pipes.filter,
823            placeholder: "Search pipes",
824            pagination: &pagination,
825            mode: app.mode,
826            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
827            is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
828        },
829    );
830
831    let start_idx = current_page * page_size;
832    let end_idx = (start_idx + page_size).min(filtered.len());
833    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
834
835    let expanded_index = app.sqs_state.pipes.expanded_item.and_then(|idx| {
836        if idx >= start_idx && idx < end_idx {
837            Some(idx - start_idx)
838        } else {
839            None
840        }
841    });
842
843    render_table(
844        frame,
845        TableConfig {
846            area: chunks[1],
847            columns: &columns,
848            items: paginated,
849            selected_index: app.sqs_state.pipes.selected % page_size.max(1),
850            is_active: app.mode != Mode::FilterInput,
851            title: format_title(&format!("EventBridge Pipes ({})", filtered.len())),
852            sort_column: "last_modified",
853            sort_direction: SortDirection::Asc,
854            expanded_index,
855            get_expanded_content: Some(Box::new(|pipe: &EventBridgePipe| {
856                expanded_from_columns(&columns, pipe)
857            })),
858        },
859    );
860}
861
862fn render_tags_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
863    use ratatui::prelude::*;
864
865    let chunks = Layout::default()
866        .direction(Direction::Vertical)
867        .constraints([Constraint::Length(3), Constraint::Min(0)])
868        .split(area);
869
870    let filtered = filtered_tags(app);
871
872    let columns: Vec<Box<dyn Column<QueueTag>>> = app
873        .sqs_state
874        .tag_visible_column_ids
875        .iter()
876        .filter_map(|id| TagColumn::from_id(id))
877        .map(|col| Box::new(col) as Box<dyn Column<QueueTag>>)
878        .collect();
879
880    let page_size = app.sqs_state.tags.page_size.value();
881    let total_pages = filtered.len().div_ceil(page_size.max(1));
882    let current_page = app.sqs_state.tags.selected / page_size.max(1);
883    let pagination = render_pagination_text(current_page, total_pages);
884
885    render_simple_filter(
886        frame,
887        chunks[0],
888        SimpleFilterConfig {
889            filter_text: &app.sqs_state.tags.filter,
890            placeholder: "Search tags",
891            pagination: &pagination,
892            mode: app.mode,
893            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
894            is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
895        },
896    );
897
898    let start_idx = current_page * page_size;
899    let end_idx = (start_idx + page_size).min(filtered.len());
900    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
901
902    let expanded_index = app.sqs_state.tags.expanded_item.and_then(|idx| {
903        if idx >= start_idx && idx < end_idx {
904            Some(idx - start_idx)
905        } else {
906            None
907        }
908    });
909
910    render_table(
911        frame,
912        TableConfig {
913            area: chunks[1],
914            columns: &columns,
915            items: paginated,
916            selected_index: app.sqs_state.tags.selected % page_size.max(1),
917            is_active: app.mode != Mode::FilterInput,
918            title: format_title(&format!("Tagging ({})", filtered.len())),
919            sort_column: "value",
920            sort_direction: SortDirection::Asc,
921            expanded_index,
922            get_expanded_content: Some(Box::new(|tag: &QueueTag| {
923                expanded_from_columns(&columns, tag)
924            })),
925        },
926    );
927}
928
929fn render_subscriptions_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
930    use ratatui::prelude::*;
931
932    let chunks = Layout::default()
933        .direction(Direction::Vertical)
934        .constraints([Constraint::Length(3), Constraint::Min(0)])
935        .split(area);
936
937    let filtered = filtered_subscriptions(app);
938
939    let columns: Vec<Box<dyn Column<SnsSubscription>>> = app
940        .sqs_state
941        .subscription_visible_column_ids
942        .iter()
943        .filter_map(|id| SubscriptionColumn::from_id(id))
944        .map(|col| Box::new(col) as Box<dyn Column<SnsSubscription>>)
945        .collect();
946
947    let page_size = app.sqs_state.subscriptions.page_size.value();
948    let total_pages = filtered.len().div_ceil(page_size.max(1));
949    let current_page = app.sqs_state.subscriptions.selected / page_size.max(1);
950    let pagination = render_pagination_text(current_page, total_pages);
951
952    // Render filter with region dropdown
953    render_subscription_filter(frame, app, chunks[0], &pagination);
954
955    let start_idx = current_page * page_size;
956    let end_idx = (start_idx + page_size).min(filtered.len());
957    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
958
959    let expanded_index = app.sqs_state.subscriptions.expanded_item.and_then(|idx| {
960        if idx >= start_idx && idx < end_idx {
961            Some(idx - start_idx)
962        } else {
963            None
964        }
965    });
966
967    render_table(
968        frame,
969        TableConfig {
970            area: chunks[1],
971            columns: &columns,
972            items: paginated,
973            selected_index: app.sqs_state.subscriptions.selected % page_size.max(1),
974            is_active: app.mode != Mode::FilterInput,
975            title: format_title(&format!("SNS subscriptions ({})", filtered.len())),
976            sort_column: "subscription_arn",
977            sort_direction: SortDirection::Asc,
978            expanded_index,
979            get_expanded_content: Some(Box::new(|sub: &SnsSubscription| {
980                expanded_from_columns(&columns, sub)
981            })),
982        },
983    );
984
985    // Render region dropdown if focused (after table so it appears on top)
986    if app.mode == FilterInput && app.sqs_state.input_focus == SUBSCRIPTION_REGION {
987        let regions = Region::all();
988        let region_codes: Vec<&str> = regions.iter().map(|r| r.code).collect();
989        render_dropdown(
990            frame,
991            &region_codes,
992            app.sqs_state.subscription_region_selected,
993            chunks[0],
994            pagination.len() as u16 + 3, // pagination + separator
995        );
996    }
997}
998
999fn render_subscription_filter(
1000    frame: &mut ratatui::Frame,
1001    app: &App,
1002    area: ratatui::prelude::Rect,
1003    pagination: &str,
1004) {
1005    let region_text = if app.sqs_state.subscription_region_filter.is_empty() {
1006        format!("Subscription region: {}", app.region)
1007    } else {
1008        format!(
1009            "Subscription region: {}",
1010            app.sqs_state.subscription_region_filter
1011        )
1012    };
1013
1014    render_filter_bar(
1015        frame,
1016        FilterConfig {
1017            filter_text: &app.sqs_state.subscriptions.filter,
1018            placeholder: "Search subscriptions",
1019            mode: app.mode,
1020            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1021            controls: vec![
1022                FilterControl {
1023                    text: region_text,
1024                    is_focused: app.sqs_state.input_focus == SUBSCRIPTION_REGION,
1025                },
1026                FilterControl {
1027                    text: pagination.to_string(),
1028                    is_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1029                },
1030            ],
1031            area,
1032        },
1033    );
1034}
1035
1036fn render_dead_letter_queue_tab(
1037    frame: &mut ratatui::Frame,
1038    app: &App,
1039    area: ratatui::prelude::Rect,
1040) {
1041    use ratatui::prelude::*;
1042
1043    let queue = app
1044        .sqs_state
1045        .queues
1046        .items
1047        .iter()
1048        .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1049
1050    if let Some(q) = queue {
1051        if !q.redrive_policy.is_empty() {
1052            if let Ok(policy) = serde_json::from_str::<serde_json::Value>(&q.redrive_policy) {
1053                let dlq_arn = policy
1054                    .get("deadLetterTargetArn")
1055                    .and_then(|v| v.as_str())
1056                    .unwrap_or("-");
1057                let max_receives = policy
1058                    .get("maxReceiveCount")
1059                    .and_then(|v| v.as_i64())
1060                    .map(|n| n.to_string())
1061                    .unwrap_or_else(|| "-".to_string());
1062
1063                let lines = vec![
1064                    labeled_field("Queue", dlq_arn),
1065                    labeled_field("Maximum receives", &max_receives),
1066                ];
1067
1068                let height = calculate_dynamic_height(&lines, area.width.saturating_sub(4)) + 2;
1069
1070                let chunks = Layout::default()
1071                    .direction(Direction::Vertical)
1072                    .constraints([Constraint::Length(height), Constraint::Min(0)])
1073                    .split(area);
1074
1075                let block = titled_block("Dead-letter queue");
1076                let inner = block.inner(chunks[0]);
1077                frame.render_widget(block, chunks[0]);
1078                render_fields_with_dynamic_columns(frame, inner, lines);
1079                return;
1080            }
1081        }
1082    }
1083
1084    // Fallback for no DLQ configured
1085    let chunks = Layout::default()
1086        .direction(Direction::Vertical)
1087        .constraints([Constraint::Length(3), Constraint::Min(0)])
1088        .split(area);
1089
1090    let block = titled_block("Dead-letter queue");
1091    let inner = block.inner(chunks[0]);
1092    frame.render_widget(block, chunks[0]);
1093    frame.render_widget(Paragraph::new("No dead-letter queue configured"), inner);
1094}
1095
1096fn render_encryption_tab(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
1097    use ratatui::prelude::*;
1098
1099    let queue = app
1100        .sqs_state
1101        .queues
1102        .items
1103        .iter()
1104        .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1105
1106    let chunks = Layout::default()
1107        .direction(Direction::Vertical)
1108        .constraints([Constraint::Length(3), Constraint::Min(0)])
1109        .split(area);
1110
1111    let block = titled_block("Encryption");
1112    let inner = block.inner(chunks[0]);
1113    frame.render_widget(block, chunks[0]);
1114
1115    if let Some(q) = queue {
1116        let encryption_text = if q.encryption.is_empty() || q.encryption == "-" {
1117            "Server-side encryption is not enabled".to_string()
1118        } else {
1119            format!("Server-side encryption is managed by {}", q.encryption)
1120        };
1121
1122        frame.render_widget(Paragraph::new(encryption_text), inner);
1123    }
1124}
1125
1126fn render_dlq_redrive_tasks_tab(
1127    frame: &mut ratatui::Frame,
1128    app: &App,
1129    area: ratatui::prelude::Rect,
1130) {
1131    use ratatui::prelude::*;
1132
1133    let queue = app
1134        .sqs_state
1135        .queues
1136        .items
1137        .iter()
1138        .find(|q| Some(&q.url) == app.sqs_state.current_queue.as_ref());
1139
1140    if let Some(q) = queue {
1141        let lines = vec![
1142            labeled_field("Name", &q.redrive_task_id),
1143            labeled_field("Date started", &q.redrive_task_start_time),
1144            labeled_field("Percent processed", &q.redrive_task_percent),
1145            labeled_field("Status", &q.redrive_task_status),
1146            labeled_field("Redrive destination", &q.redrive_task_destination),
1147        ];
1148
1149        let height = calculate_dynamic_height(&lines, area.width.saturating_sub(4)) + 2;
1150
1151        let chunks = Layout::default()
1152            .direction(Direction::Vertical)
1153            .constraints([Constraint::Length(height), Constraint::Min(0)])
1154            .split(area);
1155
1156        let block = titled_block("Dead-letter queue redrive status");
1157        let inner = block.inner(chunks[0]);
1158        frame.render_widget(block, chunks[0]);
1159        render_fields_with_dynamic_columns(frame, inner, lines);
1160    }
1161}
1162
1163fn render_queue_list(frame: &mut ratatui::Frame, app: &App, area: ratatui::prelude::Rect) {
1164    use ratatui::prelude::*;
1165    use ratatui::widgets::Clear;
1166    use SortDirection;
1167
1168    frame.render_widget(Clear, area);
1169
1170    let chunks = Layout::default()
1171        .direction(Direction::Vertical)
1172        .constraints([
1173            Constraint::Length(3), // Filter
1174            Constraint::Min(0),    // Table
1175        ])
1176        .split(area);
1177
1178    let filtered_count =
1179        filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter).len();
1180    let page_size = app.sqs_state.queues.page_size.value();
1181    let total_pages = filtered_count.div_ceil(page_size);
1182    let current_page = app.sqs_state.queues.selected / page_size;
1183    let pagination = render_pagination_text(current_page, total_pages);
1184
1185    render_simple_filter(
1186        frame,
1187        chunks[0],
1188        SimpleFilterConfig {
1189            filter_text: &app.sqs_state.queues.filter,
1190            placeholder: "Search by queue name prefix",
1191            pagination: &pagination,
1192            mode: app.mode,
1193            is_input_focused: app.sqs_state.input_focus == InputFocus::Filter,
1194            is_pagination_focused: app.sqs_state.input_focus == InputFocus::Pagination,
1195        },
1196    );
1197
1198    let filtered: Vec<_> =
1199        filtered_queues(&app.sqs_state.queues.items, &app.sqs_state.queues.filter);
1200
1201    let start_idx = current_page * page_size;
1202    let end_idx = (start_idx + page_size).min(filtered.len());
1203    let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
1204
1205    let title = format_title(&format!("Queues ({})", filtered.len()));
1206
1207    let columns: Vec<Box<dyn Column<Queue>>> = app
1208        .sqs_visible_column_ids
1209        .iter()
1210        .filter_map(|col_id| {
1211            SqsColumn::from_id(col_id).map(|col| Box::new(col) as Box<dyn Column<Queue>>)
1212        })
1213        .collect();
1214
1215    let expanded_index = app.sqs_state.queues.expanded_item.and_then(|idx| {
1216        if idx >= start_idx && idx < end_idx {
1217            Some(idx - start_idx)
1218        } else {
1219            None
1220        }
1221    });
1222
1223    let config = TableConfig {
1224        items: paginated,
1225        selected_index: app.sqs_state.queues.selected % page_size,
1226        expanded_index,
1227        columns: &columns,
1228        sort_column: "Name",
1229        sort_direction: SortDirection::Asc,
1230        title,
1231        area: chunks[1],
1232        get_expanded_content: Some(Box::new(|queue: &Queue| {
1233            expanded_from_columns(&columns, queue)
1234        })),
1235        is_active: app.mode != Mode::FilterInput,
1236    };
1237
1238    render_table(frame, config);
1239}
1240
1241#[cfg(test)]
1242mod tests {
1243    use super::*;
1244    use ratatui::prelude::Rect;
1245
1246    #[test]
1247    fn test_sqs_state_initialization() {
1248        let state = State::new();
1249        assert_eq!(state.queues.items.len(), 0);
1250        assert_eq!(state.queues.selected, 0);
1251        assert_eq!(state.queues.filter, "");
1252        assert_eq!(state.queues.page_size.value(), 50);
1253        assert_eq!(state.input_focus, InputFocus::Filter);
1254    }
1255
1256    #[test]
1257    fn test_filtered_queues_empty_filter() {
1258        let queues = vec![
1259            Queue {
1260                name: "queue1".to_string(),
1261                url: String::new(),
1262                queue_type: "Standard".to_string(),
1263                created_timestamp: String::new(),
1264                messages_available: "0".to_string(),
1265                messages_in_flight: "0".to_string(),
1266                encryption: "Disabled".to_string(),
1267                content_based_deduplication: "Disabled".to_string(),
1268                last_modified_timestamp: String::new(),
1269                visibility_timeout: String::new(),
1270                message_retention_period: String::new(),
1271                maximum_message_size: String::new(),
1272                delivery_delay: String::new(),
1273                receive_message_wait_time: String::new(),
1274                high_throughput_fifo: "N/A".to_string(),
1275                deduplication_scope: "N/A".to_string(),
1276                fifo_throughput_limit: "N/A".to_string(),
1277                dead_letter_queue: "-".to_string(),
1278                messages_delayed: "0".to_string(),
1279                redrive_allow_policy: "-".to_string(),
1280                redrive_policy: "".to_string(),
1281                redrive_task_id: "-".to_string(),
1282                redrive_task_start_time: "-".to_string(),
1283                redrive_task_status: "-".to_string(),
1284                redrive_task_percent: "-".to_string(),
1285                redrive_task_destination: "-".to_string(),
1286            },
1287            Queue {
1288                name: "queue2".to_string(),
1289                url: String::new(),
1290                queue_type: "Standard".to_string(),
1291                created_timestamp: String::new(),
1292                messages_available: "0".to_string(),
1293                messages_in_flight: "0".to_string(),
1294                encryption: "Disabled".to_string(),
1295                content_based_deduplication: "Disabled".to_string(),
1296                last_modified_timestamp: String::new(),
1297                visibility_timeout: String::new(),
1298                message_retention_period: String::new(),
1299                maximum_message_size: String::new(),
1300                delivery_delay: String::new(),
1301                receive_message_wait_time: String::new(),
1302                high_throughput_fifo: "N/A".to_string(),
1303                deduplication_scope: "N/A".to_string(),
1304                fifo_throughput_limit: "N/A".to_string(),
1305                dead_letter_queue: "-".to_string(),
1306                messages_delayed: "0".to_string(),
1307                redrive_allow_policy: "-".to_string(),
1308                redrive_policy: "".to_string(),
1309                redrive_task_id: "-".to_string(),
1310                redrive_task_start_time: "-".to_string(),
1311                redrive_task_status: "-".to_string(),
1312                redrive_task_percent: "-".to_string(),
1313                redrive_task_destination: "-".to_string(),
1314            },
1315        ];
1316
1317        let filtered = filtered_queues(&queues, "");
1318        assert_eq!(filtered.len(), 2);
1319    }
1320
1321    #[test]
1322    fn test_filtered_queues_with_prefix() {
1323        let queues = vec![
1324            Queue {
1325                name: "prod-orders".to_string(),
1326                url: String::new(),
1327                queue_type: "Standard".to_string(),
1328                created_timestamp: String::new(),
1329                messages_available: "0".to_string(),
1330                messages_in_flight: "0".to_string(),
1331                encryption: "Disabled".to_string(),
1332                content_based_deduplication: "Disabled".to_string(),
1333                last_modified_timestamp: String::new(),
1334                visibility_timeout: String::new(),
1335                message_retention_period: String::new(),
1336                maximum_message_size: String::new(),
1337                delivery_delay: String::new(),
1338                receive_message_wait_time: String::new(),
1339                high_throughput_fifo: "N/A".to_string(),
1340                deduplication_scope: "N/A".to_string(),
1341                fifo_throughput_limit: "N/A".to_string(),
1342                dead_letter_queue: "-".to_string(),
1343                messages_delayed: "0".to_string(),
1344                redrive_allow_policy: "-".to_string(),
1345                redrive_policy: "".to_string(),
1346                redrive_task_id: "-".to_string(),
1347                redrive_task_start_time: "-".to_string(),
1348                redrive_task_status: "-".to_string(),
1349                redrive_task_percent: "-".to_string(),
1350                redrive_task_destination: "-".to_string(),
1351            },
1352            Queue {
1353                name: "dev-orders".to_string(),
1354                url: String::new(),
1355                queue_type: "Standard".to_string(),
1356                created_timestamp: String::new(),
1357                messages_available: "0".to_string(),
1358                messages_in_flight: "0".to_string(),
1359                encryption: "Disabled".to_string(),
1360                content_based_deduplication: "Disabled".to_string(),
1361                last_modified_timestamp: String::new(),
1362                visibility_timeout: String::new(),
1363                message_retention_period: String::new(),
1364                maximum_message_size: String::new(),
1365                delivery_delay: String::new(),
1366                receive_message_wait_time: String::new(),
1367                high_throughput_fifo: "N/A".to_string(),
1368                deduplication_scope: "N/A".to_string(),
1369                fifo_throughput_limit: "N/A".to_string(),
1370                dead_letter_queue: "-".to_string(),
1371                messages_delayed: "0".to_string(),
1372                redrive_allow_policy: "-".to_string(),
1373                redrive_policy: "".to_string(),
1374                redrive_task_id: "-".to_string(),
1375                redrive_task_start_time: "-".to_string(),
1376                redrive_task_status: "-".to_string(),
1377                redrive_task_percent: "-".to_string(),
1378                redrive_task_destination: "-".to_string(),
1379            },
1380        ];
1381
1382        let filtered = filtered_queues(&queues, "prod");
1383        assert_eq!(filtered.len(), 1);
1384        assert_eq!(filtered[0].name, "prod-orders");
1385    }
1386
1387    #[test]
1388    fn test_filtered_queues_case_insensitive() {
1389        let queues = vec![Queue {
1390            name: "MyQueue".to_string(),
1391            url: String::new(),
1392            queue_type: "Standard".to_string(),
1393            created_timestamp: String::new(),
1394            messages_available: "0".to_string(),
1395            messages_in_flight: "0".to_string(),
1396            encryption: "Disabled".to_string(),
1397            content_based_deduplication: "Disabled".to_string(),
1398            last_modified_timestamp: String::new(),
1399            visibility_timeout: String::new(),
1400            message_retention_period: String::new(),
1401            maximum_message_size: String::new(),
1402            delivery_delay: String::new(),
1403            receive_message_wait_time: String::new(),
1404            high_throughput_fifo: "N/A".to_string(),
1405            deduplication_scope: "N/A".to_string(),
1406            fifo_throughput_limit: "N/A".to_string(),
1407            dead_letter_queue: "-".to_string(),
1408            messages_delayed: "0".to_string(),
1409            redrive_allow_policy: "-".to_string(),
1410            redrive_policy: "".to_string(),
1411            redrive_task_id: "-".to_string(),
1412            redrive_task_start_time: "-".to_string(),
1413            redrive_task_status: "-".to_string(),
1414            redrive_task_percent: "-".to_string(),
1415            redrive_task_destination: "-".to_string(),
1416        }];
1417
1418        let filtered = filtered_queues(&queues, "my");
1419        assert_eq!(filtered.len(), 1);
1420
1421        let filtered = filtered_queues(&queues, "MY");
1422        assert_eq!(filtered.len(), 1);
1423    }
1424
1425    #[test]
1426    fn test_pagination_page_size() {
1427        let state = State::new();
1428        assert_eq!(state.queues.page_size.value(), 50);
1429    }
1430
1431    #[test]
1432    fn test_state_initialization_with_policy() {
1433        let state = State::new();
1434        assert_eq!(state.policy_scroll, 0);
1435        assert_eq!(state.current_queue, None);
1436        assert!(state.policy_document.contains("Version"));
1437        assert!(state.policy_document.contains("2012-10-17"));
1438    }
1439
1440    #[test]
1441    fn test_extract_region() {
1442        let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
1443        assert_eq!(extract_region(url), "us-east-1");
1444
1445        let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
1446        assert_eq!(extract_region(url2), "eu-west-2");
1447    }
1448
1449    #[test]
1450    fn test_extract_account_id() {
1451        let url = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
1452        assert_eq!(extract_account_id(url), "123456789012");
1453
1454        let url2 = "https://sqs.eu-west-2.amazonaws.com/987654321098/TestQueue";
1455        assert_eq!(extract_account_id(url2), "987654321098");
1456    }
1457
1458    #[test]
1459    fn test_timestamp_column_width() {
1460        use SqsColumn as Column;
1461        // Timestamps are 27 characters: "YYYY-MM-DD HH:MM:SS (UTC)"
1462        assert!(Column::Created.width() >= 27);
1463        assert!(Column::LastUpdated.width() >= 27);
1464    }
1465
1466    #[test]
1467    fn test_message_retention_period_formatting() {
1468        // Test that 345600 seconds formats to days
1469        let seconds = 345600;
1470        let formatted = format_duration_seconds(seconds);
1471        // 345600 seconds = 4 days
1472        assert_eq!(formatted, "4d");
1473    }
1474
1475    #[test]
1476    fn test_queue_detail_tab_navigation() {
1477        let tab = QueueDetailTab::QueuePolicies;
1478        assert_eq!(tab.next(), QueueDetailTab::Monitoring);
1479        assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1480
1481        let tab = QueueDetailTab::Monitoring;
1482        assert_eq!(tab.next(), QueueDetailTab::SnsSubscriptions);
1483        assert_eq!(tab.prev(), QueueDetailTab::QueuePolicies);
1484
1485        let tab = QueueDetailTab::SnsSubscriptions;
1486        assert_eq!(tab.next(), QueueDetailTab::LambdaTriggers);
1487        assert_eq!(tab.prev(), QueueDetailTab::Monitoring);
1488
1489        let tab = QueueDetailTab::LambdaTriggers;
1490        assert_eq!(tab.next(), QueueDetailTab::EventBridgePipes);
1491        assert_eq!(tab.prev(), QueueDetailTab::SnsSubscriptions);
1492
1493        let tab = QueueDetailTab::EventBridgePipes;
1494        assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueue);
1495        assert_eq!(tab.prev(), QueueDetailTab::LambdaTriggers);
1496
1497        let tab = QueueDetailTab::DeadLetterQueue;
1498        assert_eq!(tab.next(), QueueDetailTab::Tagging);
1499        assert_eq!(tab.prev(), QueueDetailTab::EventBridgePipes);
1500
1501        let tab = QueueDetailTab::Tagging;
1502        assert_eq!(tab.next(), QueueDetailTab::Encryption);
1503        assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueue);
1504
1505        let tab = QueueDetailTab::Encryption;
1506        assert_eq!(tab.next(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1507        assert_eq!(tab.prev(), QueueDetailTab::Tagging);
1508
1509        let tab = QueueDetailTab::DeadLetterQueueRedriveTasks;
1510        assert_eq!(tab.next(), QueueDetailTab::QueuePolicies);
1511        assert_eq!(tab.prev(), QueueDetailTab::Encryption);
1512
1513        let tab = QueueDetailTab::QueuePolicies;
1514        assert_eq!(tab.prev(), QueueDetailTab::DeadLetterQueueRedriveTasks);
1515    }
1516
1517    #[test]
1518    fn test_queue_detail_tab_all() {
1519        let tabs = QueueDetailTab::all();
1520        assert_eq!(tabs.len(), 9);
1521        assert_eq!(tabs[0], QueueDetailTab::QueuePolicies);
1522        assert_eq!(tabs[1], QueueDetailTab::Monitoring);
1523        assert_eq!(tabs[2], QueueDetailTab::SnsSubscriptions);
1524        assert_eq!(tabs[3], QueueDetailTab::LambdaTriggers);
1525        assert_eq!(tabs[4], QueueDetailTab::EventBridgePipes);
1526        assert_eq!(tabs[5], QueueDetailTab::DeadLetterQueue);
1527        assert_eq!(tabs[6], QueueDetailTab::Tagging);
1528        assert_eq!(tabs[7], QueueDetailTab::Encryption);
1529        assert_eq!(tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
1530    }
1531
1532    #[test]
1533    fn test_queue_detail_tab_names() {
1534        assert_eq!(QueueDetailTab::QueuePolicies.name(), "Queue policies");
1535        assert_eq!(QueueDetailTab::SnsSubscriptions.name(), "SNS subscriptions");
1536        assert_eq!(QueueDetailTab::LambdaTriggers.name(), "Lambda triggers");
1537        assert_eq!(QueueDetailTab::EventBridgePipes.name(), "EventBridge Pipes");
1538        assert_eq!(QueueDetailTab::Tagging.name(), "Tagging");
1539        assert_eq!(QueueDetailTab::DeadLetterQueue.name(), "Dead-letter queue");
1540    }
1541
1542    #[test]
1543    fn test_trigger_column_all() {
1544        assert_eq!(TriggerColumn::all().len(), 4);
1545    }
1546
1547    #[test]
1548    fn test_trigger_column_ids() {
1549        let ids = TriggerColumn::ids();
1550        assert_eq!(ids.len(), 4);
1551        assert!(ids.contains(&"column.sqs.trigger.uuid"));
1552        assert!(ids.contains(&"column.sqs.trigger.arn"));
1553        assert!(ids.contains(&"column.sqs.trigger.status"));
1554        assert!(ids.contains(&"column.sqs.trigger.last_modified"));
1555    }
1556
1557    #[test]
1558    fn test_trigger_column_from_id() {
1559        assert_eq!(
1560            TriggerColumn::from_id("column.sqs.trigger.uuid"),
1561            Some(TriggerColumn::Uuid)
1562        );
1563        assert_eq!(
1564            TriggerColumn::from_id("column.sqs.trigger.arn"),
1565            Some(TriggerColumn::Arn)
1566        );
1567        assert_eq!(
1568            TriggerColumn::from_id("column.sqs.trigger.status"),
1569            Some(TriggerColumn::Status)
1570        );
1571        assert_eq!(
1572            TriggerColumn::from_id("column.sqs.trigger.last_modified"),
1573            Some(TriggerColumn::LastModified)
1574        );
1575        assert_eq!(TriggerColumn::from_id("invalid"), None);
1576    }
1577
1578    #[test]
1579    fn test_trigger_status_rendering() {
1580        use Column;
1581
1582        let trigger = LambdaTrigger {
1583            uuid: "test-uuid".to_string(),
1584            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
1585            status: "Enabled".to_string(),
1586            last_modified: "1609459200".to_string(),
1587        };
1588
1589        let (text, style) = TriggerColumn::Status.render(&trigger);
1590        assert_eq!(text, "✅ Enabled");
1591        assert_eq!(style.fg, Some(ratatui::style::Color::Green));
1592    }
1593
1594    #[test]
1595    fn test_trigger_timestamp_rendering() {
1596        use Column;
1597
1598        let trigger = LambdaTrigger {
1599            uuid: "test-uuid".to_string(),
1600            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
1601            status: "Enabled".to_string(),
1602            last_modified: "1609459200".to_string(),
1603        };
1604
1605        let (text, _) = TriggerColumn::LastModified.render(&trigger);
1606        assert!(text.contains("2021-01-01"));
1607        assert!(text.contains("(UTC)"));
1608    }
1609
1610    #[test]
1611    fn test_state_initializes_trigger_columns() {
1612        let state = State::new();
1613        assert_eq!(state.trigger_column_ids.len(), 4);
1614        assert_eq!(state.trigger_visible_column_ids.len(), 4);
1615        assert_eq!(state.trigger_column_ids, state.trigger_visible_column_ids);
1616    }
1617
1618    #[test]
1619    fn test_trigger_state_has_filter() {
1620        let mut state = State::new();
1621        state.detail_tab = QueueDetailTab::LambdaTriggers;
1622        state.triggers.filter = "test-filter".to_string();
1623
1624        // Verify filter is set
1625        assert_eq!(state.triggers.filter, "test-filter");
1626        assert_eq!(state.detail_tab, QueueDetailTab::LambdaTriggers);
1627    }
1628
1629    #[test]
1630    fn test_trigger_filtering() {
1631        let triggers = [
1632            LambdaTrigger {
1633                uuid: "uuid-123".to_string(),
1634                arn: "arn:aws:lambda:us-east-1:123:function:test1".to_string(),
1635                status: "Enabled".to_string(),
1636                last_modified: "1609459200".to_string(),
1637            },
1638            LambdaTrigger {
1639                uuid: "uuid-456".to_string(),
1640                arn: "arn:aws:lambda:us-east-1:123:function:test2".to_string(),
1641                status: "Enabled".to_string(),
1642                last_modified: "1609459200".to_string(),
1643            },
1644        ];
1645
1646        // Filter by UUID
1647        let filtered: Vec<_> = triggers.iter().filter(|t| t.uuid.contains("123")).collect();
1648        assert_eq!(filtered.len(), 1);
1649        assert_eq!(filtered[0].uuid, "uuid-123");
1650
1651        // Filter by ARN
1652        let filtered: Vec<_> = triggers
1653            .iter()
1654            .filter(|t| t.arn.contains("test2"))
1655            .collect();
1656        assert_eq!(filtered.len(), 1);
1657        assert_eq!(
1658            filtered[0].arn,
1659            "arn:aws:lambda:us-east-1:123:function:test2"
1660        );
1661    }
1662
1663    #[test]
1664    fn test_trigger_pagination() {
1665        let mut state = State::new();
1666        state.triggers.items = (0..10)
1667            .map(|i| LambdaTrigger {
1668                uuid: format!("uuid-{}", i),
1669                arn: format!("arn:aws:lambda:us-east-1:123:function:test{}", i),
1670                status: "Enabled".to_string(),
1671                last_modified: "1609459200".to_string(),
1672            })
1673            .collect();
1674
1675        assert_eq!(state.triggers.items.len(), 10);
1676        assert_eq!(state.triggers.page_size.value(), 50); // Default page size
1677    }
1678
1679    #[test]
1680    fn test_trigger_column_visibility() {
1681        let mut state = State::new();
1682
1683        // All columns visible by default
1684        assert_eq!(state.trigger_visible_column_ids.len(), 4);
1685
1686        // Remove a column
1687        state.trigger_visible_column_ids.remove(0);
1688        assert_eq!(state.trigger_visible_column_ids.len(), 3);
1689
1690        // Add it back
1691        state
1692            .trigger_visible_column_ids
1693            .push(state.trigger_column_ids[0].clone());
1694        assert_eq!(state.trigger_visible_column_ids.len(), 4);
1695    }
1696
1697    #[test]
1698    fn test_trigger_page_size_options() {
1699        let mut state = State::new();
1700
1701        // Default is 50
1702        assert_eq!(state.triggers.page_size, PageSize::Fifty);
1703
1704        // Can change to other sizes
1705        state.triggers.page_size = PageSize::Ten;
1706        assert_eq!(state.triggers.page_size.value(), 10);
1707
1708        state.triggers.page_size = PageSize::TwentyFive;
1709        assert_eq!(state.triggers.page_size.value(), 25);
1710
1711        state.triggers.page_size = PageSize::OneHundred;
1712        assert_eq!(state.triggers.page_size.value(), 100);
1713    }
1714
1715    #[test]
1716    fn test_trigger_loading_state() {
1717        let mut state = State::new();
1718
1719        // Initially not loading
1720        assert!(!state.triggers.loading);
1721
1722        // Can set to loading
1723        state.triggers.loading = true;
1724        assert!(state.triggers.loading);
1725
1726        // Can clear loading
1727        state.triggers.loading = false;
1728        assert!(!state.triggers.loading);
1729    }
1730
1731    #[test]
1732    fn test_trigger_sort_by_last_modified() {
1733        let mut triggers = [
1734            LambdaTrigger {
1735                uuid: "uuid-2".to_string(),
1736                arn: "arn2".to_string(),
1737                status: "Enabled".to_string(),
1738                last_modified: "1609459300".to_string(), // Later
1739            },
1740            LambdaTrigger {
1741                uuid: "uuid-1".to_string(),
1742                arn: "arn1".to_string(),
1743                status: "Enabled".to_string(),
1744                last_modified: "1609459200".to_string(), // Earlier
1745            },
1746        ];
1747
1748        // Sort ascending (oldest first)
1749        triggers.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
1750
1751        assert_eq!(triggers[0].uuid, "uuid-1");
1752        assert_eq!(triggers[1].uuid, "uuid-2");
1753    }
1754
1755    #[test]
1756    fn test_trigger_pagination_calculation() {
1757        let mut state = State::new();
1758
1759        // Add 25 triggers
1760        state.triggers.items = (0..25)
1761            .map(|i| LambdaTrigger {
1762                uuid: format!("uuid-{}", i),
1763                arn: format!("arn{}", i),
1764                status: "Enabled".to_string(),
1765                last_modified: "1609459200".to_string(),
1766            })
1767            .collect();
1768
1769        // With page size 10, should have 3 pages
1770        state.triggers.page_size = PageSize::Ten;
1771        let page_size = state.triggers.page_size.value();
1772        let total_pages = state.triggers.items.len().div_ceil(page_size);
1773        assert_eq!(total_pages, 3);
1774
1775        // Page 0: items 0-9
1776        let current_page = 0;
1777        let start_idx = current_page * page_size;
1778        let end_idx = (start_idx + page_size).min(state.triggers.items.len());
1779        assert_eq!(start_idx, 0);
1780        assert_eq!(end_idx, 10);
1781
1782        // Page 2: items 20-24
1783        let current_page = 2;
1784        let start_idx = current_page * page_size;
1785        let end_idx = (start_idx + page_size).min(state.triggers.items.len());
1786        assert_eq!(start_idx, 20);
1787        assert_eq!(end_idx, 25);
1788    }
1789
1790    #[test]
1791    fn test_monitoring_metric_data_with_values() {
1792        let mut state = State::new();
1793        // Mock obfuscated metric data
1794        state.metric_data = vec![
1795            (1700000000, 0.0),
1796            (1700000060, 5.0),
1797            (1700000120, 10.0),
1798            (1700000180, 0.0),
1799        ];
1800        assert_eq!(state.metric_data.len(), 4);
1801        assert_eq!(state.metric_data[0], (1700000000, 0.0));
1802        assert_eq!(state.metric_data[1], (1700000060, 5.0));
1803    }
1804
1805    #[test]
1806    fn test_monitoring_all_metrics_initialized() {
1807        let state = State::new();
1808        assert!(state.metric_data.is_empty());
1809        assert!(state.metric_data_delayed.is_empty());
1810        assert!(state.metric_data_not_visible.is_empty());
1811        assert!(state.metric_data_visible.is_empty());
1812        assert!(state.metric_data_empty_receives.is_empty());
1813        assert!(state.metric_data_messages_deleted.is_empty());
1814        assert!(state.metric_data_messages_received.is_empty());
1815        assert!(state.metric_data_messages_sent.is_empty());
1816        assert!(state.metric_data_sent_message_size.is_empty());
1817        assert_eq!(state.monitoring_scroll, 0);
1818    }
1819
1820    #[test]
1821    fn test_monitoring_scroll_pages() {
1822        let mut state = State::new();
1823        assert_eq!(state.monitoring_scroll, 0);
1824
1825        // Scroll to page 1
1826        state.monitoring_scroll = 1;
1827        assert_eq!(state.monitoring_scroll, 1);
1828
1829        // Scroll to page 2
1830        state.monitoring_scroll = 2;
1831        assert_eq!(state.monitoring_scroll, 2);
1832    }
1833
1834    #[test]
1835    fn test_monitoring_delayed_metrics() {
1836        let mut state = State::new();
1837        state.metric_data_delayed = vec![(1700000000, 1.0), (1700000060, 2.0)];
1838        assert_eq!(state.metric_data_delayed.len(), 2);
1839        assert_eq!(state.metric_data_delayed[0].1, 1.0);
1840    }
1841
1842    #[test]
1843    fn test_monitoring_not_visible_metrics() {
1844        let mut state = State::new();
1845        state.metric_data_not_visible = vec![(1700000000, 3.0), (1700000060, 4.0)];
1846        assert_eq!(state.metric_data_not_visible.len(), 2);
1847        assert_eq!(state.metric_data_not_visible[1].1, 4.0);
1848    }
1849
1850    #[test]
1851    fn test_monitoring_visible_metrics() {
1852        let mut state = State::new();
1853        state.metric_data_visible = vec![(1700000000, 5.0), (1700000060, 6.0)];
1854        assert_eq!(state.metric_data_visible.len(), 2);
1855        assert_eq!(state.metric_data_visible[0].1, 5.0);
1856    }
1857
1858    #[test]
1859    fn test_monitoring_empty_receives_metrics() {
1860        let mut state = State::new();
1861        state.metric_data_empty_receives = vec![(1700000000, 10.0), (1700000060, 15.0)];
1862        assert_eq!(state.metric_data_empty_receives.len(), 2);
1863        assert_eq!(state.metric_data_empty_receives[0].1, 10.0);
1864    }
1865
1866    #[test]
1867    fn test_monitoring_messages_deleted_metrics() {
1868        let mut state = State::new();
1869        state.metric_data_messages_deleted = vec![(1700000000, 20.0), (1700000060, 25.0)];
1870        assert_eq!(state.metric_data_messages_deleted.len(), 2);
1871        assert_eq!(state.metric_data_messages_deleted[0].1, 20.0);
1872    }
1873
1874    #[test]
1875    fn test_monitoring_messages_received_metrics() {
1876        let mut state = State::new();
1877        state.metric_data_messages_received = vec![(1700000000, 30.0), (1700000060, 35.0)];
1878        assert_eq!(state.metric_data_messages_received.len(), 2);
1879        assert_eq!(state.metric_data_messages_received[0].1, 30.0);
1880    }
1881
1882    #[test]
1883    fn test_monitoring_messages_sent_metrics() {
1884        let mut state = State::new();
1885        state.metric_data_messages_sent = vec![(1700000000, 40.0), (1700000060, 45.0)];
1886        assert_eq!(state.metric_data_messages_sent.len(), 2);
1887        assert_eq!(state.metric_data_messages_sent[0].1, 40.0);
1888    }
1889
1890    #[test]
1891    fn test_monitoring_sent_message_size_metrics() {
1892        let mut state = State::new();
1893        state.metric_data_sent_message_size = vec![(1700000000, 1024.0), (1700000060, 2048.0)];
1894        assert_eq!(state.metric_data_sent_message_size.len(), 2);
1895        assert_eq!(state.metric_data_sent_message_size[0].1, 1024.0);
1896    }
1897
1898    #[test]
1899    fn test_trigger_expand_collapse() {
1900        let mut state = State::new();
1901
1902        // Initially no item expanded
1903        assert_eq!(state.triggers.expanded_item, None);
1904
1905        // Expand item 0
1906        state.triggers.expanded_item = Some(0);
1907        assert_eq!(state.triggers.expanded_item, Some(0));
1908
1909        // Collapse (set to None)
1910        state.triggers.expanded_item = None;
1911        assert_eq!(state.triggers.expanded_item, None);
1912    }
1913
1914    #[test]
1915    fn test_trigger_filter_visibility() {
1916        let mut state = State::new();
1917
1918        // Filter starts empty
1919        assert!(state.triggers.filter.is_empty());
1920
1921        // Can set filter
1922        state.triggers.filter = "test".to_string();
1923        assert_eq!(state.triggers.filter, "test");
1924
1925        // Can clear filter
1926        state.triggers.filter.clear();
1927        assert!(state.triggers.filter.is_empty());
1928    }
1929
1930    #[test]
1931    fn test_pipe_column_ids_have_correct_prefix() {
1932        for col in PipeColumn::all() {
1933            assert!(
1934                col.id().starts_with("column.sqs.pipe."),
1935                "PipeColumn ID '{}' should start with 'column.sqs.pipe.'",
1936                col.id()
1937            );
1938        }
1939    }
1940
1941    #[test]
1942    fn test_tags_sorted_by_value() {
1943        let mut state = State::new();
1944        state.tags.items = vec![
1945            QueueTag {
1946                key: "env".to_string(),
1947                value: "prod".to_string(),
1948            },
1949            QueueTag {
1950                key: "team".to_string(),
1951                value: "backend".to_string(),
1952            },
1953            QueueTag {
1954                key: "app".to_string(),
1955                value: "api".to_string(),
1956            },
1957        ];
1958
1959        let mut sorted = state.tags.items.clone();
1960        sorted.sort_by(|a, b| a.value.cmp(&b.value));
1961
1962        assert_eq!(sorted[0].value, "api");
1963        assert_eq!(sorted[1].value, "backend");
1964        assert_eq!(sorted[2].value, "prod");
1965    }
1966
1967    #[test]
1968    fn test_tags_initialization() {
1969        let state = State::new();
1970        assert_eq!(state.tags.items.len(), 0);
1971        assert_eq!(state.tag_column_ids.len(), 2);
1972        assert_eq!(state.tag_visible_column_ids.len(), 2);
1973    }
1974
1975    #[test]
1976    fn test_queue_tag_structure() {
1977        let tag = QueueTag {
1978            key: "Environment".to_string(),
1979            value: "Production".to_string(),
1980        };
1981        assert_eq!(tag.key, "Environment");
1982        assert_eq!(tag.value, "Production");
1983    }
1984
1985    #[test]
1986    fn test_tags_table_state() {
1987        let mut state = State::new();
1988        state.tags.items = vec![
1989            QueueTag {
1990                key: "Env".to_string(),
1991                value: "prod".to_string(),
1992            },
1993            QueueTag {
1994                key: "Team".to_string(),
1995                value: "backend".to_string(),
1996            },
1997        ];
1998        assert_eq!(state.tags.items.len(), 2);
1999        assert_eq!(state.tags.selected, 0);
2000        assert_eq!(state.tags.filter, "");
2001    }
2002
2003    #[test]
2004    fn test_tags_filtering() {
2005        let tags = [
2006            QueueTag {
2007                key: "Environment".to_string(),
2008                value: "production".to_string(),
2009            },
2010            QueueTag {
2011                key: "Team".to_string(),
2012                value: "backend".to_string(),
2013            },
2014            QueueTag {
2015                key: "Project".to_string(),
2016                value: "api".to_string(),
2017            },
2018        ];
2019
2020        // Test filtering by key
2021        let filtered: Vec<_> = tags
2022            .iter()
2023            .filter(|t| t.key.to_lowercase().contains("env"))
2024            .collect();
2025        assert_eq!(filtered.len(), 1);
2026        assert_eq!(filtered[0].key, "Environment");
2027
2028        // Test filtering by value
2029        let filtered: Vec<_> = tags
2030            .iter()
2031            .filter(|t| t.value.to_lowercase().contains("back"))
2032            .collect();
2033        assert_eq!(filtered.len(), 1);
2034        assert_eq!(filtered[0].value, "backend");
2035    }
2036
2037    #[test]
2038    fn test_tags_column_ids() {
2039        let ids = TagColumn::ids();
2040        assert_eq!(ids.len(), 2);
2041        assert_eq!(ids[0], "column.sqs.tag.key");
2042        assert_eq!(ids[1], "column.sqs.tag.value");
2043    }
2044
2045    #[test]
2046    fn test_tags_column_from_id() {
2047        assert!(TagColumn::from_id("column.sqs.tag.key").is_some());
2048        assert!(TagColumn::from_id("column.sqs.tag.value").is_some());
2049        assert!(TagColumn::from_id("invalid").is_none());
2050    }
2051
2052    #[test]
2053    fn test_subscriptions_initialization() {
2054        let state = State::new();
2055        assert_eq!(state.subscriptions.items.len(), 0);
2056        assert_eq!(state.subscription_column_ids.len(), 2);
2057        assert_eq!(state.subscription_visible_column_ids.len(), 2);
2058        assert_eq!(state.subscription_region_filter, "");
2059    }
2060
2061    #[test]
2062    fn test_subscription_column_ids() {
2063        let ids = SubscriptionColumn::ids();
2064        assert_eq!(ids.len(), 2);
2065        assert_eq!(ids[0], "column.sqs.subscription.subscription_arn");
2066        assert_eq!(ids[1], "column.sqs.subscription.topic_arn");
2067    }
2068
2069    #[test]
2070    fn test_subscription_column_from_id() {
2071        assert!(SubscriptionColumn::from_id("column.sqs.subscription.subscription_arn").is_some());
2072        assert!(SubscriptionColumn::from_id("column.sqs.subscription.topic_arn").is_some());
2073        assert!(SubscriptionColumn::from_id("invalid").is_none());
2074    }
2075
2076    #[test]
2077    fn test_subscription_region_filter_default() {
2078        let state = State::new();
2079        // Default is empty string, which means use current region
2080        assert_eq!(state.subscription_region_filter, "");
2081    }
2082
2083    #[test]
2084    fn test_subscription_region_filter_display() {
2085        let mut state = State::new();
2086
2087        // When empty, should show current region
2088        assert_eq!(state.subscription_region_filter, "");
2089
2090        // When set, should show selected region
2091        state.subscription_region_filter = "us-west-2".to_string();
2092        assert_eq!(state.subscription_region_filter, "us-west-2");
2093    }
2094
2095    #[test]
2096    fn test_subscription_region_selected_index() {
2097        let state = State::new();
2098        assert_eq!(state.subscription_region_selected, 0);
2099    }
2100
2101    #[test]
2102    fn test_encryption_tab_in_all() {
2103        let tabs = QueueDetailTab::all();
2104        assert!(tabs.contains(&QueueDetailTab::Encryption));
2105    }
2106
2107    #[test]
2108    fn test_encryption_tab_name() {
2109        assert_eq!(QueueDetailTab::Encryption.name(), "Encryption");
2110    }
2111
2112    #[test]
2113    fn test_encryption_tab_order() {
2114        let tabs = QueueDetailTab::all();
2115        let dlq_idx = tabs
2116            .iter()
2117            .position(|t| *t == QueueDetailTab::DeadLetterQueue)
2118            .unwrap();
2119        let tagging_idx = tabs
2120            .iter()
2121            .position(|t| *t == QueueDetailTab::Tagging)
2122            .unwrap();
2123        let encryption_idx = tabs
2124            .iter()
2125            .position(|t| *t == QueueDetailTab::Encryption)
2126            .unwrap();
2127
2128        // Encryption should be after Tagging and DeadLetterQueue should be before Tagging
2129        assert!(dlq_idx < tagging_idx);
2130        assert!(encryption_idx > tagging_idx);
2131    }
2132
2133    #[test]
2134    fn test_dlq_redrive_tasks_tab_in_all() {
2135        let tabs = QueueDetailTab::all();
2136        assert!(tabs.contains(&QueueDetailTab::DeadLetterQueueRedriveTasks));
2137    }
2138
2139    #[test]
2140    fn test_dlq_redrive_tasks_tab_name() {
2141        assert_eq!(
2142            QueueDetailTab::DeadLetterQueueRedriveTasks.name(),
2143            "Dead-letter queue redrive tasks"
2144        );
2145    }
2146
2147    #[test]
2148    fn test_dlq_redrive_tasks_tab_order() {
2149        let tabs = QueueDetailTab::all();
2150        let encryption_idx = tabs
2151            .iter()
2152            .position(|t| *t == QueueDetailTab::Encryption)
2153            .unwrap();
2154        let redrive_idx = tabs
2155            .iter()
2156            .position(|t| *t == QueueDetailTab::DeadLetterQueueRedriveTasks)
2157            .unwrap();
2158
2159        // DeadLetterQueueRedriveTasks should be after Encryption (last tab)
2160        assert!(redrive_idx > encryption_idx);
2161        assert_eq!(redrive_idx, tabs.len() - 1);
2162    }
2163
2164    #[test]
2165    fn test_tab_strip_matches_enum_order() {
2166        // This test ensures the hardcoded tab strip in render_queue_detail matches QueueDetailTab::all()
2167        let all_tabs = QueueDetailTab::all();
2168        assert_eq!(all_tabs.len(), 9);
2169
2170        // Verify order matches
2171        assert_eq!(all_tabs[0], QueueDetailTab::QueuePolicies);
2172        assert_eq!(all_tabs[1], QueueDetailTab::Monitoring);
2173        assert_eq!(all_tabs[2], QueueDetailTab::SnsSubscriptions);
2174        assert_eq!(all_tabs[3], QueueDetailTab::LambdaTriggers);
2175        assert_eq!(all_tabs[4], QueueDetailTab::EventBridgePipes);
2176        assert_eq!(all_tabs[5], QueueDetailTab::DeadLetterQueue);
2177        assert_eq!(all_tabs[6], QueueDetailTab::Tagging);
2178        assert_eq!(all_tabs[7], QueueDetailTab::Encryption);
2179        assert_eq!(all_tabs[8], QueueDetailTab::DeadLetterQueueRedriveTasks);
2180    }
2181
2182    #[test]
2183    fn test_monitoring_tab_in_all() {
2184        let all_tabs = QueueDetailTab::all();
2185        assert!(all_tabs.contains(&QueueDetailTab::Monitoring));
2186    }
2187
2188    #[test]
2189    fn test_monitoring_tab_name() {
2190        assert_eq!(QueueDetailTab::Monitoring.name(), "Monitoring");
2191    }
2192
2193    #[test]
2194    fn test_monitoring_tab_order() {
2195        let all_tabs = QueueDetailTab::all();
2196        let monitoring_index = all_tabs
2197            .iter()
2198            .position(|t| *t == QueueDetailTab::Monitoring)
2199            .unwrap();
2200        assert_eq!(monitoring_index, 1); // Should be second, after QueuePolicies
2201    }
2202
2203    #[test]
2204    fn test_rounded_block_used_for_details_pane() {
2205        // Verify rounded_block helper creates proper bordered block
2206        let block = titled_block("Details");
2207        let area = Rect::new(0, 0, 80, 10);
2208        let inner = block.inner(area);
2209        assert_eq!(inner.width, 78); // 2 less for borders
2210        assert_eq!(inner.height, 8); // 2 less for borders
2211    }
2212}