rusticity_term/
app.rs

1pub use crate::aws::{Profile as AwsProfile, Region as AwsRegion};
2use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
3use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
4use crate::cw::insights::{InsightsFocus, InsightsState};
5pub use crate::cw::{Alarm, AlarmColumn};
6use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
7use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
8use crate::iam::{self, UserColumn};
9use crate::keymap::{Action, Mode};
10pub use crate::lambda::DeploymentColumn;
11pub use crate::lambda::ResourceColumn;
12pub use crate::lambda::{
13    Application as LambdaApplication, ApplicationColumn as LambdaApplicationColumn,
14    Function as LambdaFunction, FunctionColumn as LambdaColumn,
15};
16pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
17use crate::session::{Session, SessionTab};
18pub use crate::sqs::queue::Column as SqsColumn;
19pub use crate::sqs::trigger::Column as SqsTriggerColumn;
20use crate::table::TableState;
21pub use crate::ui::cfn::{
22    DetailTab as CfnDetailTab, State as CfnState, StatusFilter as CfnStatusFilter,
23};
24pub use crate::ui::cw::alarms::{AlarmTab, AlarmViewMode};
25pub use crate::ui::ecr::{State as EcrState, Tab as EcrTab};
26use crate::ui::iam::{GroupTab, RoleTab, State as IamState, UserTab};
27pub use crate::ui::lambda::{
28    ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
29    DetailTab as LambdaDetailTab, State as LambdaState,
30};
31pub use crate::ui::s3::{BucketType as S3BucketType, ObjectTab as S3ObjectTab, State as S3State};
32pub use crate::ui::sqs::{QueueDetailTab as SqsQueueDetailTab, State as SqsState};
33pub use crate::ui::{
34    CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, EventFilterFocus,
35    LogGroupColumn, Preferences, StreamColumn, StreamSort, TimeUnit,
36};
37use rusticity_core::{
38    AlarmsClient, AwsConfig, CloudFormationClient, CloudWatchClient, EcrClient, IamClient,
39    LambdaClient, LogEvent, LogGroup, LogStream, S3Client, SqsClient,
40};
41
42#[derive(Clone)]
43pub struct Tab {
44    pub service: Service,
45    pub title: String,
46    pub breadcrumb: String,
47}
48
49pub struct App {
50    pub running: bool,
51    pub mode: Mode,
52    pub config: AwsConfig,
53    pub cloudwatch_client: CloudWatchClient,
54    pub s3_client: S3Client,
55    pub sqs_client: SqsClient,
56    pub alarms_client: AlarmsClient,
57    pub ecr_client: EcrClient,
58    pub iam_client: IamClient,
59    pub lambda_client: LambdaClient,
60    pub cloudformation_client: CloudFormationClient,
61    pub current_service: Service,
62    pub tabs: Vec<Tab>,
63    pub current_tab: usize,
64    pub tab_picker_selected: usize,
65    pub tab_filter: String,
66    pub pending_key: Option<char>,
67    pub log_groups_state: CloudWatchLogGroupsState,
68    pub insights_state: CloudWatchInsightsState,
69    pub alarms_state: CloudWatchAlarmsState,
70    pub s3_state: S3State,
71    pub sqs_state: SqsState,
72    pub ecr_state: EcrState,
73    pub lambda_state: LambdaState,
74    pub lambda_application_state: LambdaApplicationState,
75    pub cfn_state: CfnState,
76    pub iam_state: IamState,
77    pub service_picker: ServicePickerState,
78    pub service_selected: bool,
79    pub profile: String,
80    pub region: String,
81    pub region_selector_index: usize,
82    pub cw_log_group_visible_column_ids: Vec<ColumnId>,
83    pub cw_log_group_column_ids: Vec<ColumnId>,
84    pub column_selector_index: usize,
85    pub preference_section: Preferences,
86    pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
87    pub cw_log_stream_column_ids: Vec<ColumnId>,
88    pub cw_log_event_visible_column_ids: Vec<ColumnId>,
89    pub cw_log_event_column_ids: Vec<ColumnId>,
90    pub cw_alarm_visible_column_ids: Vec<ColumnId>,
91    pub cw_alarm_column_ids: Vec<ColumnId>,
92    pub s3_bucket_visible_column_ids: Vec<ColumnId>,
93    pub s3_bucket_column_ids: Vec<ColumnId>,
94    pub sqs_visible_column_ids: Vec<ColumnId>,
95    pub sqs_column_ids: Vec<ColumnId>,
96    pub ecr_repo_visible_column_ids: Vec<ColumnId>,
97    pub ecr_repo_column_ids: Vec<ColumnId>,
98    pub ecr_image_visible_column_ids: Vec<ColumnId>,
99    pub ecr_image_column_ids: Vec<ColumnId>,
100    pub lambda_application_visible_column_ids: Vec<ColumnId>,
101    pub lambda_application_column_ids: Vec<ColumnId>,
102    pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
103    pub lambda_deployment_column_ids: Vec<ColumnId>,
104    pub lambda_resource_visible_column_ids: Vec<ColumnId>,
105    pub lambda_resource_column_ids: Vec<ColumnId>,
106    pub cfn_visible_column_ids: Vec<ColumnId>,
107    pub cfn_column_ids: Vec<ColumnId>,
108    pub iam_user_visible_column_ids: Vec<ColumnId>,
109    pub iam_user_column_ids: Vec<ColumnId>,
110    pub iam_role_visible_column_ids: Vec<String>,
111    pub iam_role_column_ids: Vec<String>,
112    pub iam_group_visible_column_ids: Vec<String>,
113    pub iam_group_column_ids: Vec<String>,
114    pub iam_policy_visible_column_ids: Vec<String>,
115    pub iam_policy_column_ids: Vec<String>,
116    pub view_mode: ViewMode,
117    pub error_message: Option<String>,
118    pub error_scroll: usize,
119    pub page_input: String,
120    pub calendar_date: Option<time::Date>,
121    pub calendar_selecting: CalendarField,
122    pub cursor_pos: usize,
123    pub current_session: Option<Session>,
124    pub sessions: Vec<Session>,
125    pub session_picker_selected: usize,
126    pub session_filter: String,
127    pub region_filter: String,
128    pub region_picker_selected: usize,
129    pub region_latencies: std::collections::HashMap<String, u64>,
130    pub profile_filter: String,
131    pub profile_picker_selected: usize,
132    pub available_profiles: Vec<AwsProfile>,
133    pub snapshot_requested: bool,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
137pub enum CalendarField {
138    StartDate,
139    EndDate,
140}
141
142pub struct CloudWatchInsightsState {
143    pub insights: InsightsState,
144    pub loading: bool,
145}
146
147pub struct CloudWatchAlarmsState {
148    pub table: TableState<Alarm>,
149    pub alarm_tab: AlarmTab,
150    pub view_as: AlarmViewMode,
151    pub wrap_lines: bool,
152    pub sort_column: String,
153    pub sort_direction: SortDirection,
154    pub input_focus: InputFocus,
155}
156
157impl PageSize {
158    pub fn value(&self) -> usize {
159        match self {
160            PageSize::Ten => 10,
161            PageSize::TwentyFive => 25,
162            PageSize::Fifty => 50,
163            PageSize::OneHundred => 100,
164        }
165    }
166
167    pub fn next(&self) -> Self {
168        match self {
169            PageSize::Ten => PageSize::TwentyFive,
170            PageSize::TwentyFive => PageSize::Fifty,
171            PageSize::Fifty => PageSize::OneHundred,
172            PageSize::OneHundred => PageSize::Ten,
173        }
174    }
175}
176
177pub struct ServicePickerState {
178    pub filter: String,
179    pub selected: usize,
180    pub services: Vec<&'static str>,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq)]
184pub enum ViewMode {
185    List,
186    Detail,
187    Events,
188    InsightsResults,
189    PolicyView,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq)]
193pub enum Service {
194    CloudWatchLogGroups,
195    CloudWatchInsights,
196    CloudWatchAlarms,
197    S3Buckets,
198    SqsQueues,
199    EcrRepositories,
200    LambdaFunctions,
201    LambdaApplications,
202    CloudFormationStacks,
203    IamUsers,
204    IamRoles,
205    IamUserGroups,
206}
207
208impl Service {
209    pub fn name(&self) -> &str {
210        match self {
211            Service::CloudWatchLogGroups => "CloudWatch > Log Groups",
212            Service::CloudWatchInsights => "CloudWatch > Logs Insights",
213            Service::CloudWatchAlarms => "CloudWatch > Alarms",
214            Service::S3Buckets => "S3 > Buckets",
215            Service::SqsQueues => "SQS > Queues",
216            Service::EcrRepositories => "ECR > Repositories",
217            Service::LambdaFunctions => "Lambda > Functions",
218            Service::LambdaApplications => "Lambda > Applications",
219            Service::CloudFormationStacks => "CloudFormation > Stacks",
220            Service::IamUsers => "IAM > Users",
221            Service::IamRoles => "IAM > Roles",
222            Service::IamUserGroups => "IAM > User Groups",
223        }
224    }
225}
226
227fn copy_to_clipboard(text: &str) {
228    use std::io::Write;
229    use std::process::{Command, Stdio};
230    if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
231        if let Some(mut stdin) = child.stdin.take() {
232            let _ = stdin.write_all(text.as_bytes());
233        }
234        let _ = child.wait();
235    }
236}
237
238fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
239    if max > 0 {
240        *selected = (*selected + page_size).min(max - 1);
241    }
242}
243
244impl App {
245    fn get_active_filter_mut(&mut self) -> Option<&mut String> {
246        if self.current_service == Service::CloudWatchAlarms {
247            Some(&mut self.alarms_state.table.filter)
248        } else if self.current_service == Service::S3Buckets {
249            if self.s3_state.current_bucket.is_some() {
250                Some(&mut self.s3_state.object_filter)
251            } else {
252                Some(&mut self.s3_state.buckets.filter)
253            }
254        } else if self.current_service == Service::EcrRepositories {
255            if self.ecr_state.current_repository.is_some() {
256                Some(&mut self.ecr_state.images.filter)
257            } else {
258                Some(&mut self.ecr_state.repositories.filter)
259            }
260        } else if self.current_service == Service::SqsQueues {
261            if self.sqs_state.current_queue.is_some()
262                && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
263            {
264                Some(&mut self.sqs_state.triggers.filter)
265            } else if self.sqs_state.current_queue.is_some()
266                && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
267            {
268                Some(&mut self.sqs_state.pipes.filter)
269            } else if self.sqs_state.current_queue.is_some()
270                && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
271            {
272                Some(&mut self.sqs_state.tags.filter)
273            } else if self.sqs_state.current_queue.is_some()
274                && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
275            {
276                Some(&mut self.sqs_state.subscriptions.filter)
277            } else {
278                Some(&mut self.sqs_state.queues.filter)
279            }
280        } else if self.current_service == Service::LambdaFunctions {
281            if self.lambda_state.current_version.is_some()
282                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
283            {
284                Some(&mut self.lambda_state.alias_table.filter)
285            } else if self.lambda_state.current_function.is_some()
286                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
287            {
288                Some(&mut self.lambda_state.version_table.filter)
289            } else if self.lambda_state.current_function.is_some()
290                && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
291            {
292                Some(&mut self.lambda_state.alias_table.filter)
293            } else {
294                Some(&mut self.lambda_state.table.filter)
295            }
296        } else if self.current_service == Service::LambdaApplications {
297            if self.lambda_application_state.current_application.is_some() {
298                if self.lambda_application_state.detail_tab
299                    == LambdaApplicationDetailTab::Deployments
300                {
301                    Some(&mut self.lambda_application_state.deployments.filter)
302                } else {
303                    Some(&mut self.lambda_application_state.resources.filter)
304                }
305            } else {
306                Some(&mut self.lambda_application_state.table.filter)
307            }
308        } else if self.current_service == Service::CloudFormationStacks {
309            Some(&mut self.cfn_state.table.filter)
310        } else if self.current_service == Service::IamUsers {
311            if self.iam_state.current_user.is_some() {
312                if self.iam_state.user_tab == UserTab::Tags {
313                    Some(&mut self.iam_state.user_tags.filter)
314                } else {
315                    Some(&mut self.iam_state.policies.filter)
316                }
317            } else {
318                Some(&mut self.iam_state.users.filter)
319            }
320        } else if self.current_service == Service::IamRoles {
321            if self.iam_state.current_role.is_some() {
322                if self.iam_state.role_tab == RoleTab::Tags {
323                    Some(&mut self.iam_state.tags.filter)
324                } else if self.iam_state.role_tab == RoleTab::LastAccessed {
325                    Some(&mut self.iam_state.last_accessed_filter)
326                } else {
327                    Some(&mut self.iam_state.policies.filter)
328                }
329            } else {
330                Some(&mut self.iam_state.roles.filter)
331            }
332        } else if self.current_service == Service::IamUserGroups {
333            if self.iam_state.current_group.is_some() {
334                if self.iam_state.group_tab == GroupTab::Permissions {
335                    Some(&mut self.iam_state.policies.filter)
336                } else if self.iam_state.group_tab == GroupTab::Users {
337                    Some(&mut self.iam_state.group_users.filter)
338                } else {
339                    None
340                }
341            } else {
342                Some(&mut self.iam_state.groups.filter)
343            }
344        } else if self.view_mode == ViewMode::List {
345            Some(&mut self.log_groups_state.log_groups.filter)
346        } else if self.view_mode == ViewMode::Detail
347            && self.log_groups_state.detail_tab == DetailTab::LogStreams
348        {
349            Some(&mut self.log_groups_state.stream_filter)
350        } else {
351            None
352        }
353    }
354
355    pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
356        let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
357            .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
358
359        std::env::set_var("AWS_PROFILE", &profile_name);
360
361        let config = AwsConfig::new(region).await?;
362        let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
363        let s3_client = S3Client::new(config.clone());
364        let sqs_client = SqsClient::new(config.clone());
365        let alarms_client = AlarmsClient::new(config.clone());
366        let ecr_client = EcrClient::new(config.clone());
367        let iam_client = IamClient::new(config.clone());
368        let lambda_client = LambdaClient::new(config.clone());
369        let cloudformation_client = CloudFormationClient::new(config.clone());
370        let region_name = config.region.clone();
371
372        Ok(Self {
373            running: true,
374            mode: Mode::ServicePicker,
375            config,
376            cloudwatch_client,
377            s3_client,
378            sqs_client,
379            alarms_client,
380            ecr_client,
381            iam_client,
382            lambda_client,
383            cloudformation_client,
384            current_service: Service::CloudWatchLogGroups,
385            tabs: Vec::new(),
386            current_tab: 0,
387            tab_picker_selected: 0,
388            tab_filter: String::new(),
389            pending_key: None,
390            log_groups_state: CloudWatchLogGroupsState::new(),
391            insights_state: CloudWatchInsightsState::new(),
392            alarms_state: CloudWatchAlarmsState::new(),
393            s3_state: S3State::new(),
394            sqs_state: SqsState::new(),
395            ecr_state: EcrState::new(),
396            lambda_state: LambdaState::new(),
397            lambda_application_state: LambdaApplicationState::new(),
398            cfn_state: CfnState::new(),
399            iam_state: IamState::new(),
400            service_picker: ServicePickerState::new(),
401            service_selected: false,
402            profile: profile_name,
403            region: region_name,
404            region_selector_index: 0,
405            cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
406            cw_log_group_column_ids: LogGroupColumn::ids(),
407            column_selector_index: 0,
408            cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
409            cw_log_stream_column_ids: StreamColumn::ids(),
410            cw_log_event_visible_column_ids: EventColumn::default_visible(),
411            cw_log_event_column_ids: EventColumn::ids(),
412            cw_alarm_visible_column_ids: [
413                AlarmColumn::Name,
414                AlarmColumn::State,
415                AlarmColumn::LastStateUpdate,
416                AlarmColumn::Conditions,
417                AlarmColumn::Actions,
418            ]
419            .iter()
420            .map(|c| c.id())
421            .collect(),
422            cw_alarm_column_ids: AlarmColumn::ids(),
423            s3_bucket_visible_column_ids: S3BucketColumn::ids(),
424            s3_bucket_column_ids: S3BucketColumn::ids(),
425            sqs_visible_column_ids: [
426                SqsColumn::Name,
427                SqsColumn::Type,
428                SqsColumn::Created,
429                SqsColumn::MessagesAvailable,
430                SqsColumn::MessagesInFlight,
431                SqsColumn::Encryption,
432                SqsColumn::ContentBasedDeduplication,
433            ]
434            .iter()
435            .map(|c| c.id())
436            .collect(),
437            sqs_column_ids: SqsColumn::ids(),
438            ecr_repo_visible_column_ids: EcrColumn::ids(),
439            ecr_repo_column_ids: EcrColumn::ids(),
440            ecr_image_visible_column_ids: EcrImageColumn::ids(),
441            ecr_image_column_ids: EcrImageColumn::ids(),
442            lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
443            lambda_application_column_ids: LambdaApplicationColumn::ids(),
444            lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
445            lambda_deployment_column_ids: DeploymentColumn::ids(),
446            lambda_resource_visible_column_ids: ResourceColumn::ids(),
447            lambda_resource_column_ids: ResourceColumn::ids(),
448            cfn_visible_column_ids: [
449                CfnColumn::Name,
450                CfnColumn::Status,
451                CfnColumn::CreatedTime,
452                CfnColumn::Description,
453            ]
454            .iter()
455            .map(|c| c.id())
456            .collect(),
457            cfn_column_ids: CfnColumn::ids(),
458            iam_user_visible_column_ids: UserColumn::visible(),
459            iam_user_column_ids: UserColumn::ids(),
460            iam_role_visible_column_ids: vec![
461                "Role name".to_string(),
462                "Trusted entities".to_string(),
463                "Creation time".to_string(),
464            ],
465            iam_role_column_ids: vec![
466                "Role name".to_string(),
467                "Path".to_string(),
468                "Trusted entities".to_string(),
469                "ARN".to_string(),
470                "Creation time".to_string(),
471                "Description".to_string(),
472                "Max session duration".to_string(),
473            ],
474            iam_group_visible_column_ids: vec![
475                "Group name".to_string(),
476                "Users".to_string(),
477                "Permissions".to_string(),
478                "Creation time".to_string(),
479            ],
480            iam_group_column_ids: vec![
481                "Group name".to_string(),
482                "Path".to_string(),
483                "Users".to_string(),
484                "Permissions".to_string(),
485                "Creation time".to_string(),
486            ],
487            iam_policy_visible_column_ids: vec![
488                "Policy name".to_string(),
489                "Type".to_string(),
490                "Attached via".to_string(),
491            ],
492            iam_policy_column_ids: vec![
493                "Policy name".to_string(),
494                "Type".to_string(),
495                "Attached via".to_string(),
496                "Attached entities".to_string(),
497                "Description".to_string(),
498                "Creation time".to_string(),
499                "Edited time".to_string(),
500            ],
501            preference_section: Preferences::Columns,
502            view_mode: ViewMode::List,
503            error_message: None,
504            error_scroll: 0,
505            page_input: String::new(),
506            calendar_date: None,
507            calendar_selecting: CalendarField::StartDate,
508            cursor_pos: 0,
509            current_session: None,
510            sessions: Vec::new(),
511            session_picker_selected: 0,
512            session_filter: String::new(),
513            region_filter: String::new(),
514            region_picker_selected: 0,
515            region_latencies: std::collections::HashMap::new(),
516            profile_filter: String::new(),
517            profile_picker_selected: 0,
518            available_profiles: Vec::new(),
519            snapshot_requested: false,
520        })
521    }
522
523    pub fn new_without_client(profile: String, region: Option<String>) -> Self {
524        let config = AwsConfig::dummy(region.clone());
525        Self {
526            running: true,
527            mode: Mode::ServicePicker,
528            config: config.clone(),
529            cloudwatch_client: CloudWatchClient::dummy(config.clone()),
530            s3_client: S3Client::new(config.clone()),
531            sqs_client: SqsClient::new(config.clone()),
532            alarms_client: AlarmsClient::new(config.clone()),
533            ecr_client: EcrClient::new(config.clone()),
534            iam_client: IamClient::new(config.clone()),
535            lambda_client: LambdaClient::new(config.clone()),
536            cloudformation_client: CloudFormationClient::new(config.clone()),
537            current_service: Service::CloudWatchLogGroups,
538            tabs: Vec::new(),
539            current_tab: 0,
540            tab_picker_selected: 0,
541            tab_filter: String::new(),
542            pending_key: None,
543            log_groups_state: CloudWatchLogGroupsState::new(),
544            insights_state: CloudWatchInsightsState::new(),
545            alarms_state: CloudWatchAlarmsState::new(),
546            s3_state: S3State::new(),
547            sqs_state: SqsState::new(),
548            ecr_state: EcrState::new(),
549            lambda_state: LambdaState::new(),
550            lambda_application_state: LambdaApplicationState::new(),
551            cfn_state: CfnState::new(),
552            iam_state: IamState::new(),
553            service_picker: ServicePickerState::new(),
554            service_selected: false,
555            profile,
556            region: region.unwrap_or_default(),
557            region_selector_index: 0,
558            cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
559            cw_log_group_column_ids: LogGroupColumn::ids(),
560            column_selector_index: 0,
561            preference_section: Preferences::Columns,
562            cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
563            cw_log_stream_column_ids: StreamColumn::ids(),
564            cw_log_event_visible_column_ids: EventColumn::default_visible(),
565            cw_log_event_column_ids: EventColumn::ids(),
566            cw_alarm_visible_column_ids: [
567                AlarmColumn::Name,
568                AlarmColumn::State,
569                AlarmColumn::LastStateUpdate,
570                AlarmColumn::Conditions,
571                AlarmColumn::Actions,
572            ]
573            .iter()
574            .map(|c| c.id())
575            .collect(),
576            cw_alarm_column_ids: AlarmColumn::ids(),
577            s3_bucket_visible_column_ids: S3BucketColumn::ids(),
578            s3_bucket_column_ids: S3BucketColumn::ids(),
579            sqs_visible_column_ids: [
580                SqsColumn::Name,
581                SqsColumn::Type,
582                SqsColumn::Created,
583                SqsColumn::MessagesAvailable,
584                SqsColumn::MessagesInFlight,
585                SqsColumn::Encryption,
586                SqsColumn::ContentBasedDeduplication,
587            ]
588            .iter()
589            .map(|c| c.id())
590            .collect(),
591            sqs_column_ids: SqsColumn::ids(),
592            ecr_repo_visible_column_ids: EcrColumn::ids(),
593            ecr_repo_column_ids: EcrColumn::ids(),
594            ecr_image_visible_column_ids: EcrImageColumn::ids(),
595            ecr_image_column_ids: EcrImageColumn::ids(),
596            lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
597            lambda_application_column_ids: LambdaApplicationColumn::ids(),
598            lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
599            lambda_deployment_column_ids: DeploymentColumn::ids(),
600            lambda_resource_visible_column_ids: ResourceColumn::ids(),
601            lambda_resource_column_ids: ResourceColumn::ids(),
602            cfn_visible_column_ids: [
603                CfnColumn::Name,
604                CfnColumn::Status,
605                CfnColumn::CreatedTime,
606                CfnColumn::Description,
607            ]
608            .iter()
609            .map(|c| c.id())
610            .collect(),
611            cfn_column_ids: CfnColumn::ids(),
612            iam_user_visible_column_ids: UserColumn::visible(),
613            iam_user_column_ids: UserColumn::ids(),
614            iam_role_visible_column_ids: vec![
615                "Role name".to_string(),
616                "Trusted entities".to_string(),
617                "Creation time".to_string(),
618            ],
619            iam_role_column_ids: vec![
620                "Role name".to_string(),
621                "Path".to_string(),
622                "Trusted entities".to_string(),
623                "ARN".to_string(),
624                "Creation time".to_string(),
625                "Description".to_string(),
626                "Max session duration".to_string(),
627            ],
628            iam_group_visible_column_ids: vec![
629                "Group name".to_string(),
630                "Users".to_string(),
631                "Permissions".to_string(),
632                "Creation time".to_string(),
633            ],
634            iam_group_column_ids: vec![
635                "Group name".to_string(),
636                "Path".to_string(),
637                "Users".to_string(),
638                "Permissions".to_string(),
639                "Creation time".to_string(),
640            ],
641            iam_policy_visible_column_ids: vec![
642                "Policy name".to_string(),
643                "Type".to_string(),
644                "Attached via".to_string(),
645            ],
646            iam_policy_column_ids: vec![
647                "Policy name".to_string(),
648                "Type".to_string(),
649                "Attached via".to_string(),
650                "Attached entities".to_string(),
651                "Description".to_string(),
652                "Creation time".to_string(),
653                "Edited time".to_string(),
654            ],
655            view_mode: ViewMode::List,
656            error_message: None,
657            error_scroll: 0,
658            page_input: String::new(),
659            calendar_date: None,
660            calendar_selecting: CalendarField::StartDate,
661            cursor_pos: 0,
662            current_session: None,
663            sessions: Vec::new(),
664            session_picker_selected: 0,
665            session_filter: String::new(),
666            region_filter: String::new(),
667            region_picker_selected: 0,
668            region_latencies: std::collections::HashMap::new(),
669            profile_filter: String::new(),
670            profile_picker_selected: 0,
671            available_profiles: Vec::new(),
672            snapshot_requested: false,
673        }
674    }
675
676    pub fn handle_action(&mut self, action: Action) {
677        match action {
678            Action::Quit => {
679                self.save_current_session();
680                self.running = false;
681            }
682            Action::CloseService => {
683                if !self.tabs.is_empty() {
684                    // Close the current tab
685                    self.tabs.remove(self.current_tab);
686
687                    if self.tabs.is_empty() {
688                        // Last tab closed - show service picker
689                        self.service_selected = false;
690                        self.current_tab = 0;
691                        self.mode = Mode::ServicePicker;
692                    } else {
693                        // Tabs remain - switch to adjacent tab
694                        if self.current_tab >= self.tabs.len() {
695                            self.current_tab = self.tabs.len() - 1;
696                        }
697                        self.current_service = self.tabs[self.current_tab].service;
698                        self.service_selected = true;
699                        self.mode = Mode::Normal;
700                    }
701                } else {
702                    // No tabs - just close service picker if open
703                    self.service_selected = false;
704                    self.mode = Mode::Normal;
705                }
706                self.service_picker.filter.clear();
707                self.service_picker.selected = 0;
708            }
709            Action::NextItem => self.next_item(),
710            Action::PrevItem => self.prev_item(),
711            Action::PageUp => self.page_up(),
712            Action::PageDown => self.page_down(),
713            Action::NextPane => self.next_pane(),
714            Action::PrevPane => self.prev_pane(),
715            Action::Select => self.select_item(),
716            Action::OpenSpaceMenu => {
717                self.mode = Mode::SpaceMenu;
718                self.service_picker.filter.clear();
719                self.service_picker.selected = 0;
720            }
721            Action::CloseMenu => {
722                self.mode = Mode::Normal;
723                self.service_picker.filter.clear();
724                // Reset selection when closing filter to avoid out-of-bounds
725                if self.current_service == Service::S3Buckets {
726                    self.s3_state.selected_row = 0;
727                    self.s3_state.selected_object = 0;
728                }
729            }
730            Action::NextTab => {
731                if !self.tabs.is_empty() {
732                    self.current_tab = (self.current_tab + 1) % self.tabs.len();
733                    self.current_service = self.tabs[self.current_tab].service;
734                }
735            }
736            Action::PrevTab => {
737                if !self.tabs.is_empty() {
738                    self.current_tab = if self.current_tab == 0 {
739                        self.tabs.len() - 1
740                    } else {
741                        self.current_tab - 1
742                    };
743                    self.current_service = self.tabs[self.current_tab].service;
744                }
745            }
746            Action::CloseTab => {
747                if !self.tabs.is_empty() {
748                    self.tabs.remove(self.current_tab);
749                    if self.tabs.is_empty() {
750                        // Last tab closed - show service picker
751                        self.service_selected = false;
752                        self.current_tab = 0;
753                        self.service_picker.filter.clear();
754                        self.service_picker.selected = 0;
755                        self.mode = Mode::ServicePicker;
756                    } else {
757                        // If we closed the last tab, move to the left
758                        // Otherwise stay at same index (which is now the next tab to the right)
759                        if self.current_tab >= self.tabs.len() {
760                            self.current_tab = self.tabs.len() - 1;
761                        }
762                        self.current_service = self.tabs[self.current_tab].service;
763                        self.service_selected = true;
764                        self.mode = Mode::Normal;
765                    }
766                }
767            }
768            Action::OpenTabPicker => {
769                if !self.tabs.is_empty() {
770                    self.tab_picker_selected = self.current_tab;
771                    self.mode = Mode::TabPicker;
772                } else {
773                    self.mode = Mode::Normal;
774                }
775            }
776            Action::OpenSessionPicker => {
777                self.save_current_session();
778                self.sessions = Session::list_all().unwrap_or_default();
779                self.session_picker_selected = 0;
780                self.mode = Mode::SessionPicker;
781            }
782            Action::LoadSession => {
783                let filtered_sessions = self.get_filtered_sessions();
784                if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
785                    let session = session.clone();
786                    // Load the session
787                    self.profile = session.profile.clone();
788                    self.region = session.region.clone();
789                    self.config.account_id = session.account_id.clone();
790                    self.config.role_arn = session.role_arn.clone();
791
792                    // Clear existing tabs and load session tabs
793                    self.tabs.clear();
794                    for session_tab in &session.tabs {
795                        // Parse service from string
796                        let service = match session_tab.service.as_str() {
797                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
798                            "CloudWatchInsights" => Service::CloudWatchInsights,
799                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
800                            "S3Buckets" => Service::S3Buckets,
801                            "SqsQueues" => Service::SqsQueues,
802                            "EcrRepositories" => Service::EcrRepositories,
803                            "LambdaFunctions" => Service::LambdaFunctions,
804                            "LambdaApplications" => Service::LambdaApplications,
805                            "CloudFormationStacks" => Service::CloudFormationStacks,
806                            "IamUsers" => Service::IamUsers,
807                            "IamRoles" => Service::IamRoles,
808                            "IamUserGroups" => Service::IamUserGroups,
809                            _ => continue,
810                        };
811
812                        self.tabs.push(Tab {
813                            service,
814                            title: session_tab.title.clone(),
815                            breadcrumb: session_tab.breadcrumb.clone(),
816                        });
817
818                        // Restore filter if present
819                        if let Some(filter) = &session_tab.filter {
820                            if service == Service::CloudWatchLogGroups {
821                                self.log_groups_state.log_groups.filter = filter.clone();
822                            }
823                        }
824                    }
825
826                    if !self.tabs.is_empty() {
827                        self.current_tab = 0;
828                        self.current_service = self.tabs[0].service;
829                        self.service_selected = true;
830                        self.current_session = Some(session.clone());
831                    }
832                }
833                self.mode = Mode::Normal;
834            }
835            Action::SaveSession => {
836                // TODO: Implement session saving
837            }
838            Action::OpenServicePicker => {
839                if self.mode == Mode::ServicePicker {
840                    self.tabs.push(Tab {
841                        service: Service::S3Buckets,
842                        title: "S3 > Buckets".to_string(),
843                        breadcrumb: "S3 > Buckets".to_string(),
844                    });
845                    self.current_tab = self.tabs.len() - 1;
846                    self.current_service = Service::S3Buckets;
847                    self.view_mode = ViewMode::List;
848                    self.service_selected = true;
849                    self.mode = Mode::Normal;
850                } else {
851                    self.mode = Mode::ServicePicker;
852                    self.service_picker.filter.clear();
853                    self.service_picker.selected = 0;
854                }
855            }
856            Action::OpenCloudWatch => {
857                self.current_service = Service::CloudWatchLogGroups;
858                self.view_mode = ViewMode::List;
859                self.service_selected = true;
860                self.mode = Mode::Normal;
861            }
862            Action::OpenCloudWatchSplit => {
863                self.current_service = Service::CloudWatchInsights;
864                self.view_mode = ViewMode::InsightsResults;
865                self.service_selected = true;
866                self.mode = Mode::Normal;
867            }
868            Action::OpenCloudWatchAlarms => {
869                self.current_service = Service::CloudWatchAlarms;
870                self.view_mode = ViewMode::List;
871                self.service_selected = true;
872                self.mode = Mode::Normal;
873            }
874            Action::FilterInput(c) => {
875                if self.mode == Mode::TabPicker {
876                    self.tab_filter.push(c);
877                    self.tab_picker_selected = 0;
878                } else if self.mode == Mode::RegionPicker {
879                    self.region_filter.push(c);
880                    self.region_picker_selected = 0;
881                } else if self.mode == Mode::ProfilePicker {
882                    self.profile_filter.push(c);
883                    self.profile_picker_selected = 0;
884                } else if self.mode == Mode::SessionPicker {
885                    self.session_filter.push(c);
886                    self.session_picker_selected = 0;
887                } else if self.mode == Mode::ServicePicker {
888                    self.service_picker.filter.push(c);
889                    self.service_picker.selected = 0;
890                } else if self.mode == Mode::InsightsInput {
891                    use crate::app::InsightsFocus;
892                    match self.insights_state.insights.insights_focus {
893                        InsightsFocus::Query => {
894                            self.insights_state.insights.query_text.push(c);
895                        }
896                        InsightsFocus::LogGroupSearch => {
897                            self.insights_state.insights.log_group_search.push(c);
898                            // Update matches
899                            if !self.insights_state.insights.log_group_search.is_empty() {
900                                self.insights_state.insights.log_group_matches = self
901                                    .log_groups_state
902                                    .log_groups
903                                    .items
904                                    .iter()
905                                    .filter(|g| {
906                                        g.name.to_lowercase().contains(
907                                            &self
908                                                .insights_state
909                                                .insights
910                                                .log_group_search
911                                                .to_lowercase(),
912                                        )
913                                    })
914                                    .take(50)
915                                    .map(|g| g.name.clone())
916                                    .collect();
917                                self.insights_state.insights.show_dropdown = true;
918                            } else {
919                                self.insights_state.insights.log_group_matches.clear();
920                                self.insights_state.insights.show_dropdown = false;
921                            }
922                        }
923                        _ => {}
924                    }
925                } else if self.mode == Mode::FilterInput {
926                    // Check if we should capture digits for page navigation
927                    let is_pagination_focused = if self.current_service
928                        == Service::LambdaApplications
929                    {
930                        if self.lambda_application_state.current_application.is_some() {
931                            if self.lambda_application_state.detail_tab
932                                == LambdaApplicationDetailTab::Deployments
933                            {
934                                self.lambda_application_state.deployment_input_focus
935                                    == InputFocus::Pagination
936                            } else {
937                                self.lambda_application_state.resource_input_focus
938                                    == InputFocus::Pagination
939                            }
940                        } else {
941                            self.lambda_application_state.input_focus == InputFocus::Pagination
942                        }
943                    } else if self.current_service == Service::CloudFormationStacks {
944                        self.cfn_state.input_focus == InputFocus::Pagination
945                    } else if self.current_service == Service::IamRoles
946                        && self.iam_state.current_role.is_none()
947                    {
948                        self.iam_state.role_input_focus == InputFocus::Pagination
949                    } else if self.view_mode == ViewMode::PolicyView {
950                        self.iam_state.policy_input_focus == InputFocus::Pagination
951                    } else if self.current_service == Service::CloudWatchAlarms {
952                        self.alarms_state.input_focus == InputFocus::Pagination
953                    } else if self.current_service == Service::CloudWatchLogGroups {
954                        self.log_groups_state.input_focus == InputFocus::Pagination
955                    } else if self.current_service == Service::EcrRepositories
956                        && self.ecr_state.current_repository.is_none()
957                    {
958                        self.ecr_state.input_focus == InputFocus::Pagination
959                    } else if self.current_service == Service::LambdaFunctions {
960                        if self.lambda_state.current_function.is_some()
961                            && self.lambda_state.detail_tab == LambdaDetailTab::Versions
962                        {
963                            self.lambda_state.version_input_focus == InputFocus::Pagination
964                        } else if self.lambda_state.current_function.is_none() {
965                            self.lambda_state.input_focus == InputFocus::Pagination
966                        } else {
967                            false
968                        }
969                    } else if self.current_service == Service::SqsQueues {
970                        if self.sqs_state.current_queue.is_some()
971                            && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
972                                || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
973                                || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
974                                || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
975                                || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
976                        {
977                            self.sqs_state.input_focus == InputFocus::Pagination
978                        } else {
979                            false
980                        }
981                    } else {
982                        false
983                    };
984
985                    if is_pagination_focused && c.is_ascii_digit() {
986                        self.page_input.push(c);
987                    } else if self.current_service == Service::LambdaApplications {
988                        let is_input_focused =
989                            if self.lambda_application_state.current_application.is_some() {
990                                if self.lambda_application_state.detail_tab
991                                    == LambdaApplicationDetailTab::Deployments
992                                {
993                                    self.lambda_application_state.deployment_input_focus
994                                        == InputFocus::Filter
995                                } else {
996                                    self.lambda_application_state.resource_input_focus
997                                        == InputFocus::Filter
998                                }
999                            } else {
1000                                self.lambda_application_state.input_focus == InputFocus::Filter
1001                            };
1002                        if is_input_focused {
1003                            if let Some(filter) = self.get_active_filter_mut() {
1004                                filter.push(c);
1005                            }
1006                        }
1007                    } else if self.current_service == Service::CloudFormationStacks {
1008                        if self.cfn_state.input_focus == InputFocus::Filter {
1009                            if let Some(filter) = self.get_active_filter_mut() {
1010                                filter.push(c);
1011                            }
1012                        }
1013                    } else if self.current_service == Service::EcrRepositories
1014                        && self.ecr_state.current_repository.is_none()
1015                    {
1016                        if self.ecr_state.input_focus == InputFocus::Filter {
1017                            if let Some(filter) = self.get_active_filter_mut() {
1018                                filter.push(c);
1019                            }
1020                        }
1021                    } else if self.current_service == Service::IamRoles
1022                        && self.iam_state.current_role.is_none()
1023                    {
1024                        if self.iam_state.role_input_focus == InputFocus::Filter {
1025                            if let Some(filter) = self.get_active_filter_mut() {
1026                                filter.push(c);
1027                            }
1028                        }
1029                    } else if self.view_mode == ViewMode::PolicyView {
1030                        if self.iam_state.policy_input_focus == InputFocus::Filter {
1031                            if let Some(filter) = self.get_active_filter_mut() {
1032                                filter.push(c);
1033                            }
1034                        }
1035                    } else if self.current_service == Service::LambdaFunctions
1036                        && self.lambda_state.current_version.is_some()
1037                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
1038                    {
1039                        if self.lambda_state.alias_input_focus == InputFocus::Filter {
1040                            if let Some(filter) = self.get_active_filter_mut() {
1041                                filter.push(c);
1042                            }
1043                        }
1044                    } else if self.current_service == Service::LambdaFunctions
1045                        && self.lambda_state.current_function.is_some()
1046                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1047                    {
1048                        if self.lambda_state.version_input_focus == InputFocus::Filter {
1049                            if let Some(filter) = self.get_active_filter_mut() {
1050                                filter.push(c);
1051                            }
1052                        }
1053                    } else if self.current_service == Service::LambdaFunctions
1054                        && self.lambda_state.current_function.is_some()
1055                        && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
1056                    {
1057                        if self.lambda_state.alias_input_focus == InputFocus::Filter {
1058                            if let Some(filter) = self.get_active_filter_mut() {
1059                                filter.push(c);
1060                            }
1061                        }
1062                    } else if self.current_service == Service::SqsQueues
1063                        && self.sqs_state.current_queue.is_some()
1064                        && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1065                            || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1066                            || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1067                            || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1068                            || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1069                    {
1070                        if self.sqs_state.input_focus == InputFocus::Filter {
1071                            if let Some(filter) = self.get_active_filter_mut() {
1072                                filter.push(c);
1073                            }
1074                        }
1075                    } else if let Some(filter) = self.get_active_filter_mut() {
1076                        filter.push(c);
1077                    }
1078                } else if self.mode == Mode::EventFilterInput {
1079                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1080                        self.log_groups_state.event_filter.push(c);
1081                    } else if c.is_ascii_digit() {
1082                        self.log_groups_state.relative_amount.push(c);
1083                    }
1084                } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1085                    self.page_input.push(c);
1086                }
1087            }
1088            Action::FilterBackspace => {
1089                if self.mode == Mode::ServicePicker {
1090                    self.service_picker.filter.pop();
1091                    self.service_picker.selected = 0;
1092                } else if self.mode == Mode::TabPicker {
1093                    self.tab_filter.pop();
1094                    self.tab_picker_selected = 0;
1095                } else if self.mode == Mode::RegionPicker {
1096                    self.region_filter.pop();
1097                    self.region_picker_selected = 0;
1098                } else if self.mode == Mode::ProfilePicker {
1099                    self.profile_filter.pop();
1100                    self.profile_picker_selected = 0;
1101                } else if self.mode == Mode::SessionPicker {
1102                    self.session_filter.pop();
1103                    self.session_picker_selected = 0;
1104                } else if self.mode == Mode::InsightsInput {
1105                    use crate::app::InsightsFocus;
1106                    match self.insights_state.insights.insights_focus {
1107                        InsightsFocus::Query => {
1108                            self.insights_state.insights.query_text.pop();
1109                        }
1110                        InsightsFocus::LogGroupSearch => {
1111                            self.insights_state.insights.log_group_search.pop();
1112                            // Update matches
1113                            if !self.insights_state.insights.log_group_search.is_empty() {
1114                                self.insights_state.insights.log_group_matches = self
1115                                    .log_groups_state
1116                                    .log_groups
1117                                    .items
1118                                    .iter()
1119                                    .filter(|g| {
1120                                        g.name.to_lowercase().contains(
1121                                            &self
1122                                                .insights_state
1123                                                .insights
1124                                                .log_group_search
1125                                                .to_lowercase(),
1126                                        )
1127                                    })
1128                                    .take(50)
1129                                    .map(|g| g.name.clone())
1130                                    .collect();
1131                                self.insights_state.insights.show_dropdown = true;
1132                            } else {
1133                                self.insights_state.insights.log_group_matches.clear();
1134                                self.insights_state.insights.show_dropdown = false;
1135                            }
1136                        }
1137                        _ => {}
1138                    }
1139                } else if self.mode == Mode::FilterInput {
1140                    // Only allow backspace when focus is on the input field
1141                    if self.current_service == Service::CloudFormationStacks {
1142                        if self.cfn_state.input_focus == InputFocus::Filter {
1143                            if let Some(filter) = self.get_active_filter_mut() {
1144                                filter.pop();
1145                            }
1146                        }
1147                    } else if let Some(filter) = self.get_active_filter_mut() {
1148                        filter.pop();
1149                    }
1150                } else if self.mode == Mode::EventFilterInput {
1151                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1152                        self.log_groups_state.event_filter.pop();
1153                    } else {
1154                        self.log_groups_state.relative_amount.pop();
1155                    }
1156                }
1157            }
1158            Action::DeleteWord => {
1159                let text = if self.mode == Mode::ServicePicker {
1160                    &mut self.service_picker.filter
1161                } else if self.mode == Mode::InsightsInput {
1162                    use crate::app::InsightsFocus;
1163                    match self.insights_state.insights.insights_focus {
1164                        InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1165                        InsightsFocus::LogGroupSearch => {
1166                            &mut self.insights_state.insights.log_group_search
1167                        }
1168                        _ => return,
1169                    }
1170                } else if self.mode == Mode::FilterInput {
1171                    if let Some(filter) = self.get_active_filter_mut() {
1172                        filter
1173                    } else {
1174                        return;
1175                    }
1176                } else if self.mode == Mode::EventFilterInput {
1177                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1178                        &mut self.log_groups_state.event_filter
1179                    } else {
1180                        &mut self.log_groups_state.relative_amount
1181                    }
1182                } else {
1183                    return;
1184                };
1185
1186                if text.is_empty() {
1187                    return;
1188                }
1189
1190                let mut chars: Vec<char> = text.chars().collect();
1191                while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1192                    chars.pop();
1193                }
1194                while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1195                    chars.pop();
1196                }
1197                *text = chars.into_iter().collect();
1198            }
1199            Action::WordLeft => {
1200                // Not implemented - would need cursor position tracking
1201            }
1202            Action::WordRight => {
1203                // Not implemented - would need cursor position tracking
1204            }
1205            Action::OpenColumnSelector => {
1206                // If we have page input, apply it instead of opening column selector
1207                if !self.page_input.is_empty() {
1208                    if let Ok(page) = self.page_input.parse::<usize>() {
1209                        self.go_to_page(page);
1210                    }
1211                    self.page_input.clear();
1212                } else {
1213                    self.mode = Mode::ColumnSelector;
1214                    self.column_selector_index = 0;
1215                }
1216            }
1217            Action::ToggleColumn => {
1218                if self.current_service == Service::S3Buckets
1219                    && self.s3_state.current_bucket.is_none()
1220                {
1221                    if let Some(col) = self.s3_bucket_column_ids.get(self.column_selector_index) {
1222                        if let Some(pos) = self
1223                            .s3_bucket_visible_column_ids
1224                            .iter()
1225                            .position(|c| c == col)
1226                        {
1227                            self.s3_bucket_visible_column_ids.remove(pos);
1228                        } else {
1229                            self.s3_bucket_visible_column_ids.push(*col);
1230                        }
1231                    }
1232                } else if self.current_service == Service::CloudWatchAlarms {
1233                    // Map flat list index to actual item
1234                    // 0: Columns header, 1-16: columns, 17: empty, 18: ViewAs header, 19-20: view options
1235                    // 21: empty, 22: PageSize header, 23-25: page sizes, 26: empty, 27: WrapLines header, 28: wrap option
1236                    let idx = self.column_selector_index;
1237                    if (1..=16).contains(&idx) {
1238                        // Column toggle
1239                        if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1240                            if let Some(pos) = self
1241                                .cw_alarm_visible_column_ids
1242                                .iter()
1243                                .position(|c| c == col)
1244                            {
1245                                self.cw_alarm_visible_column_ids.remove(pos);
1246                            } else {
1247                                self.cw_alarm_visible_column_ids.push(*col);
1248                            }
1249                        }
1250                    } else if idx == 19 {
1251                        self.alarms_state.view_as = AlarmViewMode::Table;
1252                    } else if idx == 20 {
1253                        self.alarms_state.view_as = AlarmViewMode::Cards;
1254                    } else if idx == 23 {
1255                        self.alarms_state.table.page_size = PageSize::Ten;
1256                    } else if idx == 24 {
1257                        self.alarms_state.table.page_size = PageSize::TwentyFive;
1258                    } else if idx == 25 {
1259                        self.alarms_state.table.page_size = PageSize::Fifty;
1260                    } else if idx == 26 {
1261                        self.alarms_state.table.page_size = PageSize::OneHundred;
1262                    } else if idx == 29 {
1263                        self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1264                    }
1265                } else if self.current_service == Service::EcrRepositories {
1266                    if self.ecr_state.current_repository.is_some() {
1267                        // Images view - columns + page size
1268                        let idx = self.column_selector_index;
1269                        if let Some(col) = self.ecr_image_column_ids.get(idx) {
1270                            if let Some(pos) = self
1271                                .ecr_image_visible_column_ids
1272                                .iter()
1273                                .position(|c| c == col)
1274                            {
1275                                self.ecr_image_visible_column_ids.remove(pos);
1276                            } else {
1277                                self.ecr_image_visible_column_ids.push(*col);
1278                            }
1279                        }
1280                    } else {
1281                        // Repositories view - just columns
1282                        if let Some(col) = self.ecr_repo_column_ids.get(self.column_selector_index)
1283                        {
1284                            if let Some(pos) = self
1285                                .ecr_repo_visible_column_ids
1286                                .iter()
1287                                .position(|c| c == col)
1288                            {
1289                                self.ecr_repo_visible_column_ids.remove(pos);
1290                            } else {
1291                                self.ecr_repo_visible_column_ids.push(*col);
1292                            }
1293                        }
1294                    }
1295                } else if self.current_service == Service::SqsQueues {
1296                    if self.sqs_state.current_queue.is_some()
1297                        && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1298                    {
1299                        // Triggers tab - columns + page size
1300                        let idx = self.column_selector_index;
1301                        if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
1302                            if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
1303                                if let Some(pos) = self
1304                                    .sqs_state
1305                                    .trigger_visible_column_ids
1306                                    .iter()
1307                                    .position(|c| c == col)
1308                                {
1309                                    self.sqs_state.trigger_visible_column_ids.remove(pos);
1310                                } else {
1311                                    self.sqs_state.trigger_visible_column_ids.push(col.clone());
1312                                }
1313                            }
1314                        } else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
1315                            self.sqs_state.triggers.page_size = PageSize::Ten;
1316                        } else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
1317                            self.sqs_state.triggers.page_size = PageSize::TwentyFive;
1318                        } else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
1319                            self.sqs_state.triggers.page_size = PageSize::Fifty;
1320                        } else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
1321                            self.sqs_state.triggers.page_size = PageSize::OneHundred;
1322                        }
1323                    } else if self.sqs_state.current_queue.is_some()
1324                        && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1325                    {
1326                        // Pipes tab - columns + page size
1327                        let idx = self.column_selector_index;
1328                        if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
1329                            if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
1330                                if let Some(pos) = self
1331                                    .sqs_state
1332                                    .pipe_visible_column_ids
1333                                    .iter()
1334                                    .position(|c| c == col)
1335                                {
1336                                    self.sqs_state.pipe_visible_column_ids.remove(pos);
1337                                } else {
1338                                    self.sqs_state.pipe_visible_column_ids.push(col.clone());
1339                                }
1340                            }
1341                        } else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
1342                            self.sqs_state.pipes.page_size = PageSize::Ten;
1343                        } else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
1344                            self.sqs_state.pipes.page_size = PageSize::TwentyFive;
1345                        } else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
1346                            self.sqs_state.pipes.page_size = PageSize::Fifty;
1347                        } else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
1348                            self.sqs_state.pipes.page_size = PageSize::OneHundred;
1349                        }
1350                    } else if self.sqs_state.current_queue.is_some()
1351                        && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1352                    {
1353                        // Tags tab - columns + page size
1354                        let idx = self.column_selector_index;
1355                        if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
1356                            if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
1357                                if let Some(pos) = self
1358                                    .sqs_state
1359                                    .tag_visible_column_ids
1360                                    .iter()
1361                                    .position(|c| c == col)
1362                                {
1363                                    self.sqs_state.tag_visible_column_ids.remove(pos);
1364                                } else {
1365                                    self.sqs_state.tag_visible_column_ids.push(col.clone());
1366                                }
1367                            }
1368                        } else if idx == self.sqs_state.tag_column_ids.len() + 3 {
1369                            self.sqs_state.tags.page_size = PageSize::Ten;
1370                        } else if idx == self.sqs_state.tag_column_ids.len() + 4 {
1371                            self.sqs_state.tags.page_size = PageSize::TwentyFive;
1372                        } else if idx == self.sqs_state.tag_column_ids.len() + 5 {
1373                            self.sqs_state.tags.page_size = PageSize::Fifty;
1374                        } else if idx == self.sqs_state.tag_column_ids.len() + 6 {
1375                            self.sqs_state.tags.page_size = PageSize::OneHundred;
1376                        }
1377                    } else if self.sqs_state.current_queue.is_some()
1378                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
1379                    {
1380                        // Subscriptions tab - columns + page size
1381                        let idx = self.column_selector_index;
1382                        if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
1383                            if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
1384                                if let Some(pos) = self
1385                                    .sqs_state
1386                                    .subscription_visible_column_ids
1387                                    .iter()
1388                                    .position(|c| c == col)
1389                                {
1390                                    self.sqs_state.subscription_visible_column_ids.remove(pos);
1391                                } else {
1392                                    self.sqs_state
1393                                        .subscription_visible_column_ids
1394                                        .push(col.clone());
1395                                }
1396                            }
1397                        } else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
1398                            self.sqs_state.subscriptions.page_size = PageSize::Ten;
1399                        } else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
1400                            self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
1401                        } else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
1402                            self.sqs_state.subscriptions.page_size = PageSize::Fifty;
1403                        } else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
1404                            self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
1405                        }
1406                    } else if let Some(col) = self.sqs_column_ids.get(self.column_selector_index) {
1407                        if let Some(pos) = self.sqs_visible_column_ids.iter().position(|c| c == col)
1408                        {
1409                            self.sqs_visible_column_ids.remove(pos);
1410                        } else {
1411                            self.sqs_visible_column_ids.push(*col);
1412                        }
1413                    }
1414                } else if self.current_service == Service::LambdaFunctions {
1415                    let idx = self.column_selector_index;
1416                    // Check if we're in Versions tab
1417                    if self.lambda_state.current_function.is_some()
1418                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1419                    {
1420                        // Version columns
1421                        if idx > 0 && idx <= self.lambda_state.version_column_ids.len() {
1422                            if let Some(col) = self.lambda_state.version_column_ids.get(idx - 1) {
1423                                if let Some(pos) = self
1424                                    .lambda_state
1425                                    .version_visible_column_ids
1426                                    .iter()
1427                                    .position(|c| *c == *col)
1428                                {
1429                                    self.lambda_state.version_visible_column_ids.remove(pos);
1430                                } else {
1431                                    self.lambda_state
1432                                        .version_visible_column_ids
1433                                        .push(col.clone());
1434                                }
1435                            }
1436                        } else if idx == self.lambda_state.version_column_ids.len() + 3 {
1437                            self.lambda_state.version_table.page_size = PageSize::Ten;
1438                        } else if idx == self.lambda_state.version_column_ids.len() + 4 {
1439                            self.lambda_state.version_table.page_size = PageSize::TwentyFive;
1440                        } else if idx == self.lambda_state.version_column_ids.len() + 5 {
1441                            self.lambda_state.version_table.page_size = PageSize::Fifty;
1442                        } else if idx == self.lambda_state.version_column_ids.len() + 6 {
1443                            self.lambda_state.version_table.page_size = PageSize::OneHundred;
1444                        }
1445                    } else if (self.lambda_state.current_function.is_some()
1446                        && self.lambda_state.detail_tab == LambdaDetailTab::Aliases)
1447                        || (self.lambda_state.current_version.is_some()
1448                            && self.lambda_state.detail_tab == LambdaDetailTab::Configuration)
1449                    {
1450                        // Alias columns
1451                        if idx > 0 && idx <= self.lambda_state.alias_column_ids.len() {
1452                            if let Some(col) = self.lambda_state.alias_column_ids.get(idx - 1) {
1453                                if let Some(pos) = self
1454                                    .lambda_state
1455                                    .alias_visible_column_ids
1456                                    .iter()
1457                                    .position(|c| *c == *col)
1458                                {
1459                                    self.lambda_state.alias_visible_column_ids.remove(pos);
1460                                } else {
1461                                    self.lambda_state.alias_visible_column_ids.push(col.clone());
1462                                }
1463                            }
1464                        } else if idx == self.lambda_state.alias_column_ids.len() + 3 {
1465                            self.lambda_state.alias_table.page_size = PageSize::Ten;
1466                        } else if idx == self.lambda_state.alias_column_ids.len() + 4 {
1467                            self.lambda_state.alias_table.page_size = PageSize::TwentyFive;
1468                        } else if idx == self.lambda_state.alias_column_ids.len() + 5 {
1469                            self.lambda_state.alias_table.page_size = PageSize::Fifty;
1470                        } else if idx == self.lambda_state.alias_column_ids.len() + 6 {
1471                            self.lambda_state.alias_table.page_size = PageSize::OneHundred;
1472                        }
1473                    } else {
1474                        // Function columns
1475                        if idx > 0 && idx <= self.lambda_state.function_column_ids.len() {
1476                            if let Some(col) = self.lambda_state.function_column_ids.get(idx - 1) {
1477                                if let Some(pos) = self
1478                                    .lambda_state
1479                                    .function_visible_column_ids
1480                                    .iter()
1481                                    .position(|c| *c == *col)
1482                                {
1483                                    self.lambda_state.function_visible_column_ids.remove(pos);
1484                                } else {
1485                                    self.lambda_state.function_visible_column_ids.push(*col);
1486                                }
1487                            }
1488                        } else if idx == self.lambda_state.function_column_ids.len() + 3 {
1489                            self.lambda_state.table.page_size = PageSize::Ten;
1490                        } else if idx == self.lambda_state.function_column_ids.len() + 4 {
1491                            self.lambda_state.table.page_size = PageSize::TwentyFive;
1492                        } else if idx == self.lambda_state.function_column_ids.len() + 5 {
1493                            self.lambda_state.table.page_size = PageSize::Fifty;
1494                        } else if idx == self.lambda_state.function_column_ids.len() + 6 {
1495                            self.lambda_state.table.page_size = PageSize::OneHundred;
1496                        }
1497                    }
1498                } else if self.current_service == Service::LambdaApplications {
1499                    if self.lambda_application_state.current_application.is_some() {
1500                        // In detail view - handle resource or deployment columns
1501                        if self.lambda_application_state.detail_tab
1502                            == LambdaApplicationDetailTab::Overview
1503                        {
1504                            // Resources columns
1505                            let idx = self.column_selector_index;
1506                            if idx > 0 && idx <= self.lambda_resource_column_ids.len() {
1507                                if let Some(col) = self.lambda_resource_column_ids.get(idx - 1) {
1508                                    if let Some(pos) = self
1509                                        .lambda_resource_visible_column_ids
1510                                        .iter()
1511                                        .position(|c| c == col)
1512                                    {
1513                                        self.lambda_resource_visible_column_ids.remove(pos);
1514                                    } else {
1515                                        self.lambda_resource_visible_column_ids.push(*col);
1516                                    }
1517                                }
1518                            } else if idx == self.lambda_resource_column_ids.len() + 3 {
1519                                self.lambda_application_state.resources.page_size = PageSize::Ten;
1520                            } else if idx == self.lambda_resource_column_ids.len() + 4 {
1521                                self.lambda_application_state.resources.page_size =
1522                                    PageSize::TwentyFive;
1523                            } else if idx == self.lambda_resource_column_ids.len() + 5 {
1524                                self.lambda_application_state.resources.page_size = PageSize::Fifty;
1525                            }
1526                        } else {
1527                            // Deployments columns
1528                            let idx = self.column_selector_index;
1529                            if idx > 0 && idx <= self.lambda_deployment_column_ids.len() {
1530                                if let Some(col) = self.lambda_deployment_column_ids.get(idx - 1) {
1531                                    if let Some(pos) = self
1532                                        .lambda_deployment_visible_column_ids
1533                                        .iter()
1534                                        .position(|c| c == col)
1535                                    {
1536                                        self.lambda_deployment_visible_column_ids.remove(pos);
1537                                    } else {
1538                                        self.lambda_deployment_visible_column_ids.push(*col);
1539                                    }
1540                                }
1541                            } else if idx == self.lambda_deployment_column_ids.len() + 3 {
1542                                self.lambda_application_state.deployments.page_size = PageSize::Ten;
1543                            } else if idx == self.lambda_deployment_column_ids.len() + 4 {
1544                                self.lambda_application_state.deployments.page_size =
1545                                    PageSize::TwentyFive;
1546                            } else if idx == self.lambda_deployment_column_ids.len() + 5 {
1547                                self.lambda_application_state.deployments.page_size =
1548                                    PageSize::Fifty;
1549                            }
1550                        }
1551                    } else {
1552                        // In list view - handle application columns
1553                        let idx = self.column_selector_index;
1554                        if idx > 0 && idx <= self.lambda_application_column_ids.len() {
1555                            if let Some(col) = self.lambda_application_column_ids.get(idx - 1) {
1556                                if let Some(pos) = self
1557                                    .lambda_application_visible_column_ids
1558                                    .iter()
1559                                    .position(|c| *c == *col)
1560                                {
1561                                    self.lambda_application_visible_column_ids.remove(pos);
1562                                } else {
1563                                    self.lambda_application_visible_column_ids.push(*col);
1564                                }
1565                            }
1566                        } else if idx == self.lambda_application_column_ids.len() + 3 {
1567                            self.lambda_application_state.table.page_size = PageSize::Ten;
1568                        } else if idx == self.lambda_application_column_ids.len() + 4 {
1569                            self.lambda_application_state.table.page_size = PageSize::TwentyFive;
1570                        } else if idx == self.lambda_application_column_ids.len() + 5 {
1571                            self.lambda_application_state.table.page_size = PageSize::Fifty;
1572                        }
1573                    }
1574                } else if self.view_mode == ViewMode::Events {
1575                    if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
1576                    {
1577                        if let Some(pos) = self
1578                            .cw_log_event_visible_column_ids
1579                            .iter()
1580                            .position(|c| c == col)
1581                        {
1582                            self.cw_log_event_visible_column_ids.remove(pos);
1583                        } else {
1584                            self.cw_log_event_visible_column_ids.push(*col);
1585                        }
1586                    }
1587                } else if self.view_mode == ViewMode::Detail {
1588                    if let Some(col) = self
1589                        .cw_log_stream_column_ids
1590                        .get(self.column_selector_index)
1591                    {
1592                        if let Some(pos) = self
1593                            .cw_log_stream_visible_column_ids
1594                            .iter()
1595                            .position(|c| c == col)
1596                        {
1597                            self.cw_log_stream_visible_column_ids.remove(pos);
1598                        } else {
1599                            self.cw_log_stream_visible_column_ids.push(*col);
1600                        }
1601                    }
1602                } else if self.current_service == Service::CloudFormationStacks {
1603                    let idx = self.column_selector_index;
1604                    if idx > 0 && idx <= self.cfn_column_ids.len() {
1605                        if let Some(col) = self.cfn_column_ids.get(idx - 1) {
1606                            if let Some(pos) =
1607                                self.cfn_visible_column_ids.iter().position(|c| c == col)
1608                            {
1609                                self.cfn_visible_column_ids.remove(pos);
1610                            } else {
1611                                self.cfn_visible_column_ids.push(*col);
1612                            }
1613                        }
1614                    } else if idx == self.cfn_column_ids.len() + 3 {
1615                        self.cfn_state.table.page_size = PageSize::Ten;
1616                    } else if idx == self.cfn_column_ids.len() + 4 {
1617                        self.cfn_state.table.page_size = PageSize::TwentyFive;
1618                    } else if idx == self.cfn_column_ids.len() + 5 {
1619                        self.cfn_state.table.page_size = PageSize::Fifty;
1620                    } else if idx == self.cfn_column_ids.len() + 6 {
1621                        self.cfn_state.table.page_size = PageSize::OneHundred;
1622                    }
1623                } else if self.current_service == Service::IamUsers {
1624                    let idx = self.column_selector_index;
1625                    if self.iam_state.current_user.is_some() {
1626                        // Policy columns
1627                        if idx > 0 && idx <= self.iam_policy_column_ids.len() {
1628                            if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
1629                                if let Some(pos) = self
1630                                    .iam_policy_visible_column_ids
1631                                    .iter()
1632                                    .position(|c| c == col)
1633                                {
1634                                    self.iam_policy_visible_column_ids.remove(pos);
1635                                } else {
1636                                    self.iam_policy_visible_column_ids.push(col.clone());
1637                                }
1638                            }
1639                        } else if idx == self.iam_policy_column_ids.len() + 3 {
1640                            self.iam_state.policies.page_size = PageSize::Ten;
1641                        } else if idx == self.iam_policy_column_ids.len() + 4 {
1642                            self.iam_state.policies.page_size = PageSize::TwentyFive;
1643                        } else if idx == self.iam_policy_column_ids.len() + 5 {
1644                            self.iam_state.policies.page_size = PageSize::Fifty;
1645                        }
1646                    } else {
1647                        // User columns
1648                        if idx > 0 && idx <= self.iam_user_column_ids.len() {
1649                            if let Some(col) = self.iam_user_column_ids.get(idx - 1) {
1650                                if let Some(pos) = self
1651                                    .iam_user_visible_column_ids
1652                                    .iter()
1653                                    .position(|c| c == col)
1654                                {
1655                                    self.iam_user_visible_column_ids.remove(pos);
1656                                } else {
1657                                    self.iam_user_visible_column_ids.push(*col);
1658                                }
1659                            }
1660                        } else if idx == self.iam_user_column_ids.len() + 3 {
1661                            self.iam_state.users.page_size = PageSize::Ten;
1662                        } else if idx == self.iam_user_column_ids.len() + 4 {
1663                            self.iam_state.users.page_size = PageSize::TwentyFive;
1664                        } else if idx == self.iam_user_column_ids.len() + 5 {
1665                            self.iam_state.users.page_size = PageSize::Fifty;
1666                        }
1667                    }
1668                } else if self.current_service == Service::IamRoles {
1669                    let idx = self.column_selector_index;
1670                    if self.iam_state.current_role.is_some() {
1671                        // Policy columns
1672                        if idx > 0 && idx <= self.iam_policy_column_ids.len() {
1673                            if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
1674                                if let Some(pos) = self
1675                                    .iam_policy_visible_column_ids
1676                                    .iter()
1677                                    .position(|c| c == col)
1678                                {
1679                                    self.iam_policy_visible_column_ids.remove(pos);
1680                                } else {
1681                                    self.iam_policy_visible_column_ids.push(col.clone());
1682                                }
1683                            }
1684                        } else if idx == self.iam_policy_column_ids.len() + 3 {
1685                            self.iam_state.policies.page_size = PageSize::Ten;
1686                        } else if idx == self.iam_policy_column_ids.len() + 4 {
1687                            self.iam_state.policies.page_size = PageSize::TwentyFive;
1688                        } else if idx == self.iam_policy_column_ids.len() + 5 {
1689                            self.iam_state.policies.page_size = PageSize::Fifty;
1690                        }
1691                    } else {
1692                        // Role columns
1693                        if idx > 0 && idx <= self.iam_role_column_ids.len() {
1694                            if let Some(col) = self.iam_role_column_ids.get(idx - 1) {
1695                                if let Some(pos) = self
1696                                    .iam_role_visible_column_ids
1697                                    .iter()
1698                                    .position(|c| c == col)
1699                                {
1700                                    self.iam_role_visible_column_ids.remove(pos);
1701                                } else {
1702                                    self.iam_role_visible_column_ids.push(col.clone());
1703                                }
1704                            }
1705                        } else if idx == self.iam_role_column_ids.len() + 3 {
1706                            self.iam_state.roles.page_size = PageSize::Ten;
1707                        } else if idx == self.iam_role_column_ids.len() + 4 {
1708                            self.iam_state.roles.page_size = PageSize::TwentyFive;
1709                        } else if idx == self.iam_role_column_ids.len() + 5 {
1710                            self.iam_state.roles.page_size = PageSize::Fifty;
1711                        }
1712                    }
1713                } else if self.current_service == Service::IamUserGroups {
1714                    let idx = self.column_selector_index;
1715                    if idx > 0 && idx <= self.iam_group_column_ids.len() {
1716                        if let Some(col) = self.iam_group_column_ids.get(idx - 1) {
1717                            if let Some(pos) = self
1718                                .iam_group_visible_column_ids
1719                                .iter()
1720                                .position(|c| c == col)
1721                            {
1722                                self.iam_group_visible_column_ids.remove(pos);
1723                            } else {
1724                                self.iam_group_visible_column_ids.push(col.clone());
1725                            }
1726                        }
1727                    } else if idx == self.iam_group_column_ids.len() + 3 {
1728                        self.iam_state.groups.page_size = PageSize::Ten;
1729                    } else if idx == self.iam_group_column_ids.len() + 4 {
1730                        self.iam_state.groups.page_size = PageSize::TwentyFive;
1731                    } else if idx == self.iam_group_column_ids.len() + 5 {
1732                        self.iam_state.groups.page_size = PageSize::Fifty;
1733                    }
1734                } else if let Some(col) =
1735                    self.cw_log_group_column_ids.get(self.column_selector_index)
1736                {
1737                    if let Some(pos) = self
1738                        .cw_log_group_visible_column_ids
1739                        .iter()
1740                        .position(|c| c == col)
1741                    {
1742                        self.cw_log_group_visible_column_ids.remove(pos);
1743                    } else {
1744                        self.cw_log_group_visible_column_ids.push(*col);
1745                    }
1746                }
1747            }
1748            Action::NextPreferences => {
1749                if self.current_service == Service::CloudWatchAlarms {
1750                    // Jump to next section: Columns(0), ViewAs(18), PageSize(22), WrapLines(28)
1751                    if self.column_selector_index < 18 {
1752                        self.column_selector_index = 18; // ViewAs header
1753                    } else if self.column_selector_index < 22 {
1754                        self.column_selector_index = 22; // PageSize header
1755                    } else if self.column_selector_index < 28 {
1756                        self.column_selector_index = 28; // WrapLines header
1757                    } else {
1758                        self.column_selector_index = 0; // Back to Columns header
1759                    }
1760                } else if self.current_service == Service::EcrRepositories
1761                    && self.ecr_state.current_repository.is_some()
1762                {
1763                    // Images view: Columns(0), PageSize(columns.len() + 2)
1764                    let page_size_idx = self.ecr_image_column_ids.len() + 2;
1765                    if self.column_selector_index < page_size_idx {
1766                        self.column_selector_index = page_size_idx;
1767                    } else {
1768                        self.column_selector_index = 0;
1769                    }
1770                } else if self.current_service == Service::LambdaFunctions {
1771                    // Lambda: Columns(0), PageSize(columns.len() + 2)
1772                    let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
1773                    if self.column_selector_index < page_size_idx {
1774                        self.column_selector_index = page_size_idx;
1775                    } else {
1776                        self.column_selector_index = 0;
1777                    }
1778                } else if self.current_service == Service::LambdaApplications {
1779                    // Lambda Applications: Columns(0), PageSize(columns.len() + 2)
1780                    let page_size_idx = self.lambda_application_column_ids.len() + 2;
1781                    if self.column_selector_index < page_size_idx {
1782                        self.column_selector_index = page_size_idx;
1783                    } else {
1784                        self.column_selector_index = 0;
1785                    }
1786                } else if self.current_service == Service::CloudFormationStacks {
1787                    // CloudFormation: Columns(0), PageSize(columns.len() + 2)
1788                    let page_size_idx = self.cfn_column_ids.len() + 2;
1789                    if self.column_selector_index < page_size_idx {
1790                        self.column_selector_index = page_size_idx;
1791                    } else {
1792                        self.column_selector_index = 0;
1793                    }
1794                } else if self.current_service == Service::IamUsers {
1795                    if self.iam_state.current_user.is_some() {
1796                        // Only Permissions tab has column preferences
1797                        if self.iam_state.user_tab == UserTab::Permissions {
1798                            let page_size_idx = self.iam_policy_column_ids.len() + 2;
1799                            if self.column_selector_index < page_size_idx {
1800                                self.column_selector_index = page_size_idx;
1801                            } else {
1802                                self.column_selector_index = 0;
1803                            }
1804                        }
1805                        // Other tabs (Groups, Tags, Security Credentials, Last Accessed) have no preferences
1806                    } else {
1807                        // User columns: Columns(0), PageSize(columns.len() + 2)
1808                        let page_size_idx = self.iam_user_column_ids.len() + 2;
1809                        if self.column_selector_index < page_size_idx {
1810                            self.column_selector_index = page_size_idx;
1811                        } else {
1812                            self.column_selector_index = 0;
1813                        }
1814                    }
1815                } else if self.current_service == Service::IamRoles {
1816                    if self.iam_state.current_role.is_some() {
1817                        // Policy columns: Columns(0), PageSize(columns.len() + 2)
1818                        let page_size_idx = self.iam_policy_column_ids.len() + 2;
1819                        if self.column_selector_index < page_size_idx {
1820                            self.column_selector_index = page_size_idx;
1821                        } else {
1822                            self.column_selector_index = 0;
1823                        }
1824                    } else {
1825                        // Role columns: Columns(0), PageSize(columns.len() + 2)
1826                        let page_size_idx = self.iam_role_column_ids.len() + 2;
1827                        if self.column_selector_index < page_size_idx {
1828                            self.column_selector_index = page_size_idx;
1829                        } else {
1830                            self.column_selector_index = 0;
1831                        }
1832                    }
1833                } else if self.current_service == Service::IamUserGroups {
1834                    // Group columns: Columns(0), PageSize(columns.len() + 2)
1835                    let page_size_idx = self.iam_group_column_ids.len() + 2;
1836                    if self.column_selector_index < page_size_idx {
1837                        self.column_selector_index = page_size_idx;
1838                    } else {
1839                        self.column_selector_index = 0;
1840                    }
1841                } else if self.current_service == Service::SqsQueues
1842                    && self.sqs_state.current_queue.is_some()
1843                    && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1844                {
1845                    // Triggers tab: Columns(0), PageSize(columns.len() + 2)
1846                    let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
1847                    if self.column_selector_index < page_size_idx {
1848                        self.column_selector_index = page_size_idx;
1849                    } else {
1850                        self.column_selector_index = 0;
1851                    }
1852                } else if self.current_service == Service::SqsQueues
1853                    && self.sqs_state.current_queue.is_some()
1854                    && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1855                {
1856                    // Pipes tab: Columns(0), PageSize(columns.len() + 2)
1857                    let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
1858                    if self.column_selector_index < page_size_idx {
1859                        self.column_selector_index = page_size_idx;
1860                    } else {
1861                        self.column_selector_index = 0;
1862                    }
1863                } else if self.current_service == Service::SqsQueues
1864                    && self.sqs_state.current_queue.is_some()
1865                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1866                {
1867                    // Tags tab: Columns(0), PageSize(columns.len() + 2)
1868                    let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
1869                    if self.column_selector_index < page_size_idx {
1870                        self.column_selector_index = page_size_idx;
1871                    } else {
1872                        self.column_selector_index = 0;
1873                    }
1874                } else if self.current_service == Service::SqsQueues
1875                    && self.sqs_state.current_queue.is_some()
1876                    && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
1877                {
1878                    // Subscriptions tab: Columns(0), PageSize(columns.len() + 2)
1879                    let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
1880                    if self.column_selector_index < page_size_idx {
1881                        self.column_selector_index = page_size_idx;
1882                    } else {
1883                        self.column_selector_index = 0;
1884                    }
1885                }
1886            }
1887            Action::CloseColumnSelector => {
1888                self.mode = Mode::Normal;
1889                self.preference_section = Preferences::Columns;
1890            }
1891            Action::NextDetailTab => {
1892                if self.current_service == Service::SqsQueues
1893                    && self.sqs_state.current_queue.is_some()
1894                {
1895                    self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
1896                    if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
1897                        self.sqs_state.metrics_loading = true;
1898                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
1899                        self.sqs_state.triggers.loading = true;
1900                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
1901                        self.sqs_state.pipes.loading = true;
1902                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
1903                        self.sqs_state.tags.loading = true;
1904                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
1905                        self.sqs_state.subscriptions.loading = true;
1906                    }
1907                } else if self.current_service == Service::LambdaApplications
1908                    && self.lambda_application_state.current_application.is_some()
1909                {
1910                    self.lambda_application_state.detail_tab =
1911                        self.lambda_application_state.detail_tab.next();
1912                } else if self.current_service == Service::IamRoles
1913                    && self.iam_state.current_role.is_some()
1914                {
1915                    self.iam_state.role_tab = self.iam_state.role_tab.next();
1916                    if self.iam_state.role_tab == RoleTab::Tags {
1917                        self.iam_state.tags.loading = true;
1918                    }
1919                } else if self.current_service == Service::IamUsers
1920                    && self.iam_state.current_user.is_some()
1921                {
1922                    self.iam_state.user_tab = self.iam_state.user_tab.next();
1923                    if self.iam_state.user_tab == UserTab::Tags {
1924                        self.iam_state.user_tags.loading = true;
1925                    }
1926                } else if self.current_service == Service::IamUserGroups
1927                    && self.iam_state.current_group.is_some()
1928                {
1929                    self.iam_state.group_tab = self.iam_state.group_tab.next();
1930                } else if self.view_mode == ViewMode::Detail {
1931                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
1932                } else if self.current_service == Service::S3Buckets {
1933                    if self.s3_state.current_bucket.is_some() {
1934                        self.s3_state.object_tab = self.s3_state.object_tab.next();
1935                    } else {
1936                        self.s3_state.bucket_type = match self.s3_state.bucket_type {
1937                            S3BucketType::GeneralPurpose => S3BucketType::Directory,
1938                            S3BucketType::Directory => S3BucketType::GeneralPurpose,
1939                        };
1940                        self.s3_state.buckets.reset();
1941                    }
1942                } else if self.current_service == Service::CloudWatchAlarms {
1943                    self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
1944                        AlarmTab::AllAlarms => AlarmTab::InAlarm,
1945                        AlarmTab::InAlarm => AlarmTab::AllAlarms,
1946                    };
1947                    self.alarms_state.table.reset();
1948                } else if self.current_service == Service::EcrRepositories
1949                    && self.ecr_state.current_repository.is_none()
1950                {
1951                    self.ecr_state.tab = self.ecr_state.tab.next();
1952                    self.ecr_state.repositories.reset();
1953                    self.ecr_state.repositories.loading = true;
1954                } else if self.current_service == Service::LambdaFunctions
1955                    && self.lambda_state.current_function.is_some()
1956                {
1957                    if self.lambda_state.current_version.is_some() {
1958                        // Version view: only Code and Configuration tabs
1959                        self.lambda_state.detail_tab = match self.lambda_state.detail_tab {
1960                            LambdaDetailTab::Code => LambdaDetailTab::Configuration,
1961                            _ => LambdaDetailTab::Code,
1962                        };
1963                    } else {
1964                        self.lambda_state.detail_tab = self.lambda_state.detail_tab.next();
1965                    }
1966                } else if self.current_service == Service::CloudFormationStacks
1967                    && self.cfn_state.current_stack.is_some()
1968                {
1969                    self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
1970                }
1971            }
1972            Action::PrevDetailTab => {
1973                if self.current_service == Service::SqsQueues
1974                    && self.sqs_state.current_queue.is_some()
1975                {
1976                    self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
1977                    if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
1978                        self.sqs_state.metrics_loading = true;
1979                    }
1980                } else if self.current_service == Service::LambdaApplications
1981                    && self.lambda_application_state.current_application.is_some()
1982                {
1983                    self.lambda_application_state.detail_tab =
1984                        self.lambda_application_state.detail_tab.prev();
1985                } else if self.current_service == Service::IamRoles
1986                    && self.iam_state.current_role.is_some()
1987                {
1988                    self.iam_state.role_tab = self.iam_state.role_tab.prev();
1989                } else if self.current_service == Service::IamUsers
1990                    && self.iam_state.current_user.is_some()
1991                {
1992                    self.iam_state.user_tab = self.iam_state.user_tab.prev();
1993                } else if self.current_service == Service::IamUserGroups
1994                    && self.iam_state.current_group.is_some()
1995                {
1996                    self.iam_state.group_tab = self.iam_state.group_tab.prev();
1997                } else if self.view_mode == ViewMode::Detail {
1998                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
1999                } else if self.current_service == Service::S3Buckets {
2000                    if self.s3_state.current_bucket.is_some() {
2001                        self.s3_state.object_tab = self.s3_state.object_tab.prev();
2002                    }
2003                } else if self.current_service == Service::CloudWatchAlarms {
2004                    self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2005                        AlarmTab::AllAlarms => AlarmTab::InAlarm,
2006                        AlarmTab::InAlarm => AlarmTab::AllAlarms,
2007                    };
2008                } else if self.current_service == Service::EcrRepositories
2009                    && self.ecr_state.current_repository.is_none()
2010                {
2011                    self.ecr_state.tab = self.ecr_state.tab.prev();
2012                    self.ecr_state.repositories.reset();
2013                    self.ecr_state.repositories.loading = true;
2014                } else if self.current_service == Service::LambdaFunctions
2015                    && self.lambda_state.current_function.is_some()
2016                {
2017                    if self.lambda_state.current_version.is_some() {
2018                        // Version view: only Code and Configuration tabs
2019                        self.lambda_state.detail_tab = match self.lambda_state.detail_tab {
2020                            LambdaDetailTab::Configuration => LambdaDetailTab::Code,
2021                            _ => LambdaDetailTab::Configuration,
2022                        };
2023                    } else {
2024                        self.lambda_state.detail_tab = self.lambda_state.detail_tab.prev();
2025                    }
2026                } else if self.current_service == Service::CloudFormationStacks
2027                    && self.cfn_state.current_stack.is_some()
2028                {
2029                    self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
2030                }
2031            }
2032            Action::StartFilter => {
2033                // Don't allow filter mode when no service is selected and no tabs open
2034                if !self.service_selected && self.tabs.is_empty() {
2035                    return;
2036                }
2037
2038                if self.current_service == Service::CloudWatchInsights {
2039                    self.mode = Mode::InsightsInput;
2040                } else if self.current_service == Service::CloudWatchAlarms {
2041                    self.mode = Mode::FilterInput;
2042                } else if self.current_service == Service::S3Buckets {
2043                    self.mode = Mode::FilterInput;
2044                    self.log_groups_state.filter_mode = true;
2045                } else if self.current_service == Service::EcrRepositories
2046                    || self.current_service == Service::IamUsers
2047                    || self.current_service == Service::IamUserGroups
2048                {
2049                    self.mode = Mode::FilterInput;
2050                    if self.current_service == Service::EcrRepositories
2051                        && self.ecr_state.current_repository.is_none()
2052                    {
2053                        self.ecr_state.input_focus = InputFocus::Filter;
2054                    }
2055                } else if self.current_service == Service::LambdaFunctions {
2056                    self.mode = Mode::FilterInput;
2057                    if self.lambda_state.current_version.is_some()
2058                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2059                    {
2060                        self.lambda_state.alias_input_focus = InputFocus::Filter;
2061                    } else if self.lambda_state.current_function.is_some()
2062                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2063                    {
2064                        self.lambda_state.version_input_focus = InputFocus::Filter;
2065                    } else if self.lambda_state.current_function.is_none() {
2066                        self.lambda_state.input_focus = InputFocus::Filter;
2067                    }
2068                } else if self.current_service == Service::LambdaApplications {
2069                    self.mode = Mode::FilterInput;
2070                    if self.lambda_application_state.current_application.is_some() {
2071                        // In detail view - check which tab
2072                        if self.lambda_application_state.detail_tab
2073                            == LambdaApplicationDetailTab::Overview
2074                        {
2075                            self.lambda_application_state.resource_input_focus = InputFocus::Filter;
2076                        } else {
2077                            self.lambda_application_state.deployment_input_focus =
2078                                InputFocus::Filter;
2079                        }
2080                    } else {
2081                        self.lambda_application_state.input_focus = InputFocus::Filter;
2082                    }
2083                } else if self.current_service == Service::IamRoles {
2084                    self.mode = Mode::FilterInput;
2085                } else if self.current_service == Service::CloudFormationStacks {
2086                    self.mode = Mode::FilterInput;
2087                    self.cfn_state.input_focus = InputFocus::Filter;
2088                } else if self.current_service == Service::SqsQueues {
2089                    self.mode = Mode::FilterInput;
2090                    self.sqs_state.input_focus = InputFocus::Filter;
2091                } else if self.view_mode == ViewMode::List
2092                    || (self.view_mode == ViewMode::Detail
2093                        && self.log_groups_state.detail_tab == DetailTab::LogStreams)
2094                {
2095                    self.mode = Mode::FilterInput;
2096                    self.log_groups_state.filter_mode = true;
2097                    self.log_groups_state.input_focus = InputFocus::Filter;
2098                }
2099            }
2100            Action::StartEventFilter => {
2101                if self.current_service == Service::CloudWatchInsights {
2102                    self.mode = Mode::InsightsInput;
2103                } else if self.view_mode == ViewMode::List {
2104                    self.mode = Mode::FilterInput;
2105                    self.log_groups_state.filter_mode = true;
2106                    self.log_groups_state.input_focus = InputFocus::Filter;
2107                } else if self.view_mode == ViewMode::Events {
2108                    self.mode = Mode::EventFilterInput;
2109                } else if self.view_mode == ViewMode::Detail
2110                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2111                {
2112                    self.mode = Mode::FilterInput;
2113                    self.log_groups_state.filter_mode = true;
2114                    self.log_groups_state.input_focus = InputFocus::Filter;
2115                }
2116            }
2117            Action::NextFilterFocus => {
2118                if self.mode == Mode::FilterInput
2119                    && self.current_service == Service::LambdaApplications
2120                {
2121                    use crate::ui::lambda::FILTER_CONTROLS;
2122                    if self.lambda_application_state.current_application.is_some() {
2123                        if self.lambda_application_state.detail_tab
2124                            == LambdaApplicationDetailTab::Deployments
2125                        {
2126                            self.lambda_application_state.deployment_input_focus = self
2127                                .lambda_application_state
2128                                .deployment_input_focus
2129                                .next(&FILTER_CONTROLS);
2130                        } else {
2131                            self.lambda_application_state.resource_input_focus = self
2132                                .lambda_application_state
2133                                .resource_input_focus
2134                                .next(&FILTER_CONTROLS);
2135                        }
2136                    } else {
2137                        self.lambda_application_state.input_focus = self
2138                            .lambda_application_state
2139                            .input_focus
2140                            .next(&FILTER_CONTROLS);
2141                    }
2142                } else if self.mode == Mode::FilterInput
2143                    && self.current_service == Service::IamRoles
2144                    && self.iam_state.current_role.is_some()
2145                {
2146                    use crate::ui::iam::POLICY_FILTER_CONTROLS;
2147                    self.iam_state.policy_input_focus = self
2148                        .iam_state
2149                        .policy_input_focus
2150                        .next(&POLICY_FILTER_CONTROLS);
2151                } else if self.mode == Mode::FilterInput
2152                    && self.current_service == Service::IamRoles
2153                    && self.iam_state.current_role.is_none()
2154                {
2155                    use crate::ui::iam::ROLE_FILTER_CONTROLS;
2156                    self.iam_state.role_input_focus =
2157                        self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
2158                } else if self.mode == Mode::InsightsInput {
2159                    use crate::app::InsightsFocus;
2160                    self.insights_state.insights.insights_focus =
2161                        match self.insights_state.insights.insights_focus {
2162                            InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
2163                            InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
2164                            InsightsFocus::LogGroupSearch => InsightsFocus::Query,
2165                            InsightsFocus::Query => InsightsFocus::QueryLanguage,
2166                        };
2167                } else if self.mode == Mode::FilterInput
2168                    && self.current_service == Service::CloudFormationStacks
2169                {
2170                    self.cfn_state.input_focus = self
2171                        .cfn_state
2172                        .input_focus
2173                        .next(&crate::ui::cfn::State::FILTER_CONTROLS);
2174                } else if self.mode == Mode::FilterInput
2175                    && self.current_service == Service::SqsQueues
2176                {
2177                    if self.sqs_state.current_queue.is_some()
2178                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2179                    {
2180                        use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
2181                        self.sqs_state.input_focus = self
2182                            .sqs_state
2183                            .input_focus
2184                            .next(SUBSCRIPTION_FILTER_CONTROLS);
2185                    } else {
2186                        use crate::ui::sqs::FILTER_CONTROLS;
2187                        self.sqs_state.input_focus =
2188                            self.sqs_state.input_focus.next(FILTER_CONTROLS);
2189                    }
2190                } else if self.mode == Mode::FilterInput
2191                    && self.current_service == Service::CloudWatchLogGroups
2192                {
2193                    use crate::ui::cw::logs::FILTER_CONTROLS;
2194                    self.log_groups_state.input_focus =
2195                        self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
2196                } else if self.mode == Mode::EventFilterInput {
2197                    self.log_groups_state.event_input_focus =
2198                        self.log_groups_state.event_input_focus.next();
2199                } else if self.mode == Mode::FilterInput
2200                    && self.current_service == Service::CloudWatchAlarms
2201                {
2202                    use crate::ui::cw::alarms::FILTER_CONTROLS;
2203                    self.alarms_state.input_focus =
2204                        self.alarms_state.input_focus.next(&FILTER_CONTROLS);
2205                } else if self.mode == Mode::FilterInput
2206                    && self.current_service == Service::EcrRepositories
2207                    && self.ecr_state.current_repository.is_none()
2208                {
2209                    use crate::ui::ecr::FILTER_CONTROLS;
2210                    self.ecr_state.input_focus = self.ecr_state.input_focus.next(&FILTER_CONTROLS);
2211                } else if self.mode == Mode::FilterInput
2212                    && self.current_service == Service::LambdaFunctions
2213                {
2214                    use crate::ui::lambda::FILTER_CONTROLS;
2215                    if self.lambda_state.current_version.is_some()
2216                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2217                    {
2218                        self.lambda_state.alias_input_focus =
2219                            self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
2220                    } else if self.lambda_state.current_function.is_some()
2221                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2222                    {
2223                        self.lambda_state.version_input_focus =
2224                            self.lambda_state.version_input_focus.next(&FILTER_CONTROLS);
2225                    } else if self.lambda_state.current_function.is_some()
2226                        && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
2227                    {
2228                        self.lambda_state.alias_input_focus =
2229                            self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
2230                    } else if self.lambda_state.current_function.is_none() {
2231                        self.lambda_state.input_focus =
2232                            self.lambda_state.input_focus.next(&FILTER_CONTROLS);
2233                    }
2234                }
2235            }
2236            Action::PrevFilterFocus => {
2237                if self.mode == Mode::FilterInput
2238                    && self.current_service == Service::LambdaApplications
2239                {
2240                    use crate::ui::lambda::FILTER_CONTROLS;
2241                    if self.lambda_application_state.current_application.is_some() {
2242                        if self.lambda_application_state.detail_tab
2243                            == LambdaApplicationDetailTab::Deployments
2244                        {
2245                            self.lambda_application_state.deployment_input_focus = self
2246                                .lambda_application_state
2247                                .deployment_input_focus
2248                                .prev(&FILTER_CONTROLS);
2249                        } else {
2250                            self.lambda_application_state.resource_input_focus = self
2251                                .lambda_application_state
2252                                .resource_input_focus
2253                                .prev(&FILTER_CONTROLS);
2254                        }
2255                    } else {
2256                        self.lambda_application_state.input_focus = self
2257                            .lambda_application_state
2258                            .input_focus
2259                            .prev(&FILTER_CONTROLS);
2260                    }
2261                } else if self.mode == Mode::FilterInput
2262                    && self.current_service == Service::CloudFormationStacks
2263                {
2264                    self.cfn_state.input_focus = self
2265                        .cfn_state
2266                        .input_focus
2267                        .prev(&crate::ui::cfn::State::FILTER_CONTROLS);
2268                } else if self.mode == Mode::FilterInput
2269                    && self.current_service == Service::SqsQueues
2270                {
2271                    if self.sqs_state.current_queue.is_some()
2272                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2273                    {
2274                        use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
2275                        self.sqs_state.input_focus = self
2276                            .sqs_state
2277                            .input_focus
2278                            .prev(SUBSCRIPTION_FILTER_CONTROLS);
2279                    } else {
2280                        use crate::ui::sqs::FILTER_CONTROLS;
2281                        self.sqs_state.input_focus =
2282                            self.sqs_state.input_focus.prev(FILTER_CONTROLS);
2283                    }
2284                } else if self.mode == Mode::FilterInput
2285                    && self.current_service == Service::IamRoles
2286                    && self.iam_state.current_role.is_none()
2287                {
2288                    use crate::ui::iam::ROLE_FILTER_CONTROLS;
2289                    self.iam_state.role_input_focus =
2290                        self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
2291                } else if self.mode == Mode::FilterInput
2292                    && self.current_service == Service::CloudWatchLogGroups
2293                {
2294                    use crate::ui::cw::logs::FILTER_CONTROLS;
2295                    self.log_groups_state.input_focus =
2296                        self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
2297                } else if self.mode == Mode::EventFilterInput {
2298                    self.log_groups_state.event_input_focus =
2299                        self.log_groups_state.event_input_focus.prev();
2300                } else if self.mode == Mode::FilterInput
2301                    && self.current_service == Service::IamRoles
2302                    && self.iam_state.current_role.is_some()
2303                {
2304                    use crate::ui::iam::POLICY_FILTER_CONTROLS;
2305                    self.iam_state.policy_input_focus = self
2306                        .iam_state
2307                        .policy_input_focus
2308                        .prev(&POLICY_FILTER_CONTROLS);
2309                } else if self.mode == Mode::FilterInput
2310                    && self.current_service == Service::CloudWatchAlarms
2311                {
2312                    use crate::ui::cw::alarms::FILTER_CONTROLS;
2313                    self.alarms_state.input_focus =
2314                        self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
2315                } else if self.mode == Mode::FilterInput
2316                    && self.current_service == Service::EcrRepositories
2317                    && self.ecr_state.current_repository.is_none()
2318                {
2319                    use crate::ui::ecr::FILTER_CONTROLS;
2320                    self.ecr_state.input_focus = self.ecr_state.input_focus.prev(&FILTER_CONTROLS);
2321                } else if self.mode == Mode::FilterInput
2322                    && self.current_service == Service::LambdaFunctions
2323                {
2324                    use crate::ui::lambda::FILTER_CONTROLS;
2325                    if self.lambda_state.current_version.is_some()
2326                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
2327                    {
2328                        self.lambda_state.alias_input_focus =
2329                            self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2330                    } else if self.lambda_state.current_function.is_some()
2331                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2332                    {
2333                        self.lambda_state.version_input_focus =
2334                            self.lambda_state.version_input_focus.prev(&FILTER_CONTROLS);
2335                    } else if self.lambda_state.current_function.is_some()
2336                        && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
2337                    {
2338                        self.lambda_state.alias_input_focus =
2339                            self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2340                    } else if self.lambda_state.current_function.is_none() {
2341                        self.lambda_state.input_focus =
2342                            self.lambda_state.input_focus.prev(&FILTER_CONTROLS);
2343                    }
2344                }
2345            }
2346            Action::ToggleFilterCheckbox => {
2347                if self.mode == Mode::InsightsInput {
2348                    use crate::app::InsightsFocus;
2349                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2350                        && self.insights_state.insights.show_dropdown
2351                        && !self.insights_state.insights.log_group_matches.is_empty()
2352                    {
2353                        let selected_idx = self.insights_state.insights.dropdown_selected;
2354                        if let Some(group_name) = self
2355                            .insights_state
2356                            .insights
2357                            .log_group_matches
2358                            .get(selected_idx)
2359                        {
2360                            let group_name = group_name.clone();
2361                            if let Some(pos) = self
2362                                .insights_state
2363                                .insights
2364                                .selected_log_groups
2365                                .iter()
2366                                .position(|g| g == &group_name)
2367                            {
2368                                self.insights_state.insights.selected_log_groups.remove(pos);
2369                            } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2370                                self.insights_state
2371                                    .insights
2372                                    .selected_log_groups
2373                                    .push(group_name);
2374                            }
2375                        }
2376                    }
2377                } else if self.mode == Mode::FilterInput
2378                    && self.current_service == Service::CloudFormationStacks
2379                {
2380                    use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
2381                    match self.cfn_state.input_focus {
2382                        STATUS_FILTER => {
2383                            self.cfn_state.status_filter = self.cfn_state.status_filter.next();
2384                        }
2385                        VIEW_NESTED => {
2386                            self.cfn_state.view_nested = !self.cfn_state.view_nested;
2387                        }
2388                        _ => {}
2389                    }
2390                } else if self.mode == Mode::FilterInput
2391                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2392                {
2393                    match self.log_groups_state.input_focus {
2394                        InputFocus::Checkbox("ExactMatch") => {
2395                            self.log_groups_state.exact_match = !self.log_groups_state.exact_match
2396                        }
2397                        InputFocus::Checkbox("ShowExpired") => {
2398                            self.log_groups_state.show_expired = !self.log_groups_state.show_expired
2399                        }
2400                        _ => {}
2401                    }
2402                } else if self.mode == Mode::EventFilterInput
2403                    && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
2404                {
2405                    self.log_groups_state.relative_unit =
2406                        self.log_groups_state.relative_unit.next();
2407                }
2408            }
2409            Action::CycleSortColumn => {
2410                if self.view_mode == ViewMode::Detail
2411                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2412                {
2413                    self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
2414                        StreamSort::Name => StreamSort::CreationTime,
2415                        StreamSort::CreationTime => StreamSort::LastEventTime,
2416                        StreamSort::LastEventTime => StreamSort::Name,
2417                    };
2418                }
2419            }
2420            Action::ToggleSortDirection => {
2421                if self.view_mode == ViewMode::Detail
2422                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2423                {
2424                    self.log_groups_state.stream_sort_desc =
2425                        !self.log_groups_state.stream_sort_desc;
2426                }
2427            }
2428            Action::ScrollUp => {
2429                if self.mode == Mode::ErrorModal {
2430                    self.error_scroll = self.error_scroll.saturating_sub(1);
2431                } else if self.current_service == Service::SqsQueues
2432                    && self.sqs_state.current_queue.is_some()
2433                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2434                {
2435                    self.sqs_state.monitoring_scroll =
2436                        self.sqs_state.monitoring_scroll.saturating_sub(5);
2437                } else if self.view_mode == ViewMode::PolicyView {
2438                    self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
2439                } else if self.current_service == Service::IamRoles
2440                    && self.iam_state.current_role.is_some()
2441                    && self.iam_state.role_tab == RoleTab::TrustRelationships
2442                {
2443                    self.iam_state.trust_policy_scroll =
2444                        self.iam_state.trust_policy_scroll.saturating_sub(10);
2445                } else if self.view_mode == ViewMode::Events {
2446                    if self.log_groups_state.event_scroll_offset == 0
2447                        && self.log_groups_state.has_older_events
2448                    {
2449                        self.log_groups_state.loading = true;
2450                    } else {
2451                        self.log_groups_state.event_scroll_offset =
2452                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
2453                    }
2454                } else if self.view_mode == ViewMode::InsightsResults {
2455                    self.insights_state.insights.results_selected = self
2456                        .insights_state
2457                        .insights
2458                        .results_selected
2459                        .saturating_sub(1);
2460                } else if self.view_mode == ViewMode::Detail {
2461                    self.log_groups_state.selected_stream =
2462                        self.log_groups_state.selected_stream.saturating_sub(1);
2463                    self.log_groups_state.expanded_stream = None;
2464                } else if self.view_mode == ViewMode::List
2465                    && self.current_service == Service::CloudWatchLogGroups
2466                {
2467                    self.log_groups_state.log_groups.selected =
2468                        self.log_groups_state.log_groups.selected.saturating_sub(1);
2469                    self.log_groups_state.log_groups.snap_to_page();
2470                } else if self.current_service == Service::EcrRepositories {
2471                    if self.ecr_state.current_repository.is_some() {
2472                        self.ecr_state.images.page_up();
2473                    } else {
2474                        self.ecr_state.repositories.page_up();
2475                    }
2476                }
2477            }
2478            Action::ScrollDown => {
2479                if self.mode == Mode::ErrorModal {
2480                    if let Some(error_msg) = &self.error_message {
2481                        let lines = error_msg.lines().count();
2482                        let max_scroll = lines.saturating_sub(1);
2483                        self.error_scroll = (self.error_scroll + 1).min(max_scroll);
2484                    }
2485                } else if self.current_service == Service::SqsQueues
2486                    && self.sqs_state.current_queue.is_some()
2487                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2488                {
2489                    self.sqs_state.monitoring_scroll =
2490                        (self.sqs_state.monitoring_scroll + 1).min(1);
2491                } else if self.view_mode == ViewMode::PolicyView {
2492                    let lines = self.iam_state.policy_document.lines().count();
2493                    let max_scroll = lines.saturating_sub(1);
2494                    self.iam_state.policy_scroll =
2495                        (self.iam_state.policy_scroll + 10).min(max_scroll);
2496                } else if self.current_service == Service::IamRoles
2497                    && self.iam_state.current_role.is_some()
2498                    && self.iam_state.role_tab == RoleTab::TrustRelationships
2499                {
2500                    let lines = self.iam_state.trust_policy_document.lines().count();
2501                    let max_scroll = lines.saturating_sub(1);
2502                    self.iam_state.trust_policy_scroll =
2503                        (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
2504                } else if self.view_mode == ViewMode::Events {
2505                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
2506                    if self.log_groups_state.event_scroll_offset >= max_scroll {
2507                        // At the end, do nothing
2508                    } else {
2509                        self.log_groups_state.event_scroll_offset =
2510                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
2511                    }
2512                } else if self.view_mode == ViewMode::InsightsResults {
2513                    let max = self
2514                        .insights_state
2515                        .insights
2516                        .query_results
2517                        .len()
2518                        .saturating_sub(1);
2519                    self.insights_state.insights.results_selected =
2520                        (self.insights_state.insights.results_selected + 1).min(max);
2521                } else if self.view_mode == ViewMode::Detail {
2522                    let filtered_streams = self.filtered_log_streams();
2523                    let max = filtered_streams.len().saturating_sub(1);
2524                    self.log_groups_state.selected_stream =
2525                        (self.log_groups_state.selected_stream + 1).min(max);
2526                } else if self.view_mode == ViewMode::List
2527                    && self.current_service == Service::CloudWatchLogGroups
2528                {
2529                    let filtered_groups = self.filtered_log_groups();
2530                    self.log_groups_state
2531                        .log_groups
2532                        .next_item(filtered_groups.len());
2533                } else if self.current_service == Service::EcrRepositories {
2534                    if self.ecr_state.current_repository.is_some() {
2535                        let filtered_images = self.filtered_ecr_images();
2536                        self.ecr_state.images.page_down(filtered_images.len());
2537                    } else {
2538                        let filtered_repos = self.filtered_ecr_repositories();
2539                        self.ecr_state.repositories.page_down(filtered_repos.len());
2540                    }
2541                }
2542            }
2543
2544            Action::Refresh => {
2545                if self.mode == Mode::ProfilePicker {
2546                    self.log_groups_state.loading = true;
2547                    self.log_groups_state.loading_message = "Refreshing...".to_string();
2548                } else if self.mode == Mode::RegionPicker {
2549                    self.measure_region_latencies();
2550                } else if self.mode == Mode::SessionPicker {
2551                    self.sessions = Session::list_all().unwrap_or_default();
2552                } else if self.current_service == Service::CloudWatchInsights
2553                    && !self.insights_state.insights.selected_log_groups.is_empty()
2554                {
2555                    self.log_groups_state.loading = true;
2556                    self.insights_state.insights.query_completed = true;
2557                } else if self.current_service == Service::LambdaFunctions {
2558                    self.lambda_state.table.loading = true;
2559                } else if self.current_service == Service::LambdaApplications {
2560                    self.lambda_application_state.table.loading = true;
2561                } else if matches!(
2562                    self.view_mode,
2563                    ViewMode::Events | ViewMode::Detail | ViewMode::List
2564                ) {
2565                    self.log_groups_state.loading = true;
2566                }
2567            }
2568            Action::Yank => {
2569                if self.mode == Mode::ErrorModal {
2570                    // Copy error message
2571                    if let Some(error) = &self.error_message {
2572                        copy_to_clipboard(error);
2573                    }
2574                } else if self.view_mode == ViewMode::Events {
2575                    if let Some(event) = self
2576                        .log_groups_state
2577                        .log_events
2578                        .get(self.log_groups_state.event_scroll_offset)
2579                    {
2580                        copy_to_clipboard(&event.message);
2581                    }
2582                } else if self.current_service == Service::EcrRepositories {
2583                    if self.ecr_state.current_repository.is_some() {
2584                        let filtered_images = self.filtered_ecr_images();
2585                        if let Some(image) = self.ecr_state.images.get_selected(&filtered_images) {
2586                            copy_to_clipboard(&image.uri);
2587                        }
2588                    } else {
2589                        let filtered_repos = self.filtered_ecr_repositories();
2590                        if let Some(repo) =
2591                            self.ecr_state.repositories.get_selected(&filtered_repos)
2592                        {
2593                            copy_to_clipboard(&repo.uri);
2594                        }
2595                    }
2596                } else if self.current_service == Service::LambdaFunctions {
2597                    let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
2598                    if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
2599                        copy_to_clipboard(&func.arn);
2600                    }
2601                } else if self.current_service == Service::CloudFormationStacks {
2602                    if let Some(stack_name) = &self.cfn_state.current_stack {
2603                        // In detail view - copy current stack ARN
2604                        if let Some(stack) = self
2605                            .cfn_state
2606                            .table
2607                            .items
2608                            .iter()
2609                            .find(|s| &s.name == stack_name)
2610                        {
2611                            copy_to_clipboard(&stack.stack_id);
2612                        }
2613                    } else {
2614                        // In list view - copy selected stack ARN
2615                        let filtered_stacks = self.filtered_cloudformation_stacks();
2616                        if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
2617                            copy_to_clipboard(&stack.stack_id);
2618                        }
2619                    }
2620                } else if self.current_service == Service::IamUsers {
2621                    if self.iam_state.current_user.is_some() {
2622                        if let Some(user_name) = &self.iam_state.current_user {
2623                            if let Some(user) = self
2624                                .iam_state
2625                                .users
2626                                .items
2627                                .iter()
2628                                .find(|u| u.user_name == *user_name)
2629                            {
2630                                copy_to_clipboard(&user.arn);
2631                            }
2632                        }
2633                    } else {
2634                        let filtered_users = crate::ui::iam::filtered_iam_users(self);
2635                        if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
2636                            copy_to_clipboard(&user.arn);
2637                        }
2638                    }
2639                } else if self.current_service == Service::IamRoles {
2640                    if self.iam_state.current_role.is_some() {
2641                        if let Some(role_name) = &self.iam_state.current_role {
2642                            if let Some(role) = self
2643                                .iam_state
2644                                .roles
2645                                .items
2646                                .iter()
2647                                .find(|r| r.role_name == *role_name)
2648                            {
2649                                copy_to_clipboard(&role.arn);
2650                            }
2651                        }
2652                    } else {
2653                        let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
2654                        if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
2655                            copy_to_clipboard(&role.arn);
2656                        }
2657                    }
2658                } else if self.current_service == Service::IamUserGroups {
2659                    if self.iam_state.current_group.is_some() {
2660                        if let Some(group_name) = &self.iam_state.current_group {
2661                            let arn = iam::format_arn(&self.config.account_id, "group", group_name);
2662                            copy_to_clipboard(&arn);
2663                        }
2664                    } else {
2665                        let filtered_groups: Vec<_> = self
2666                            .iam_state
2667                            .groups
2668                            .items
2669                            .iter()
2670                            .filter(|g| {
2671                                if self.iam_state.groups.filter.is_empty() {
2672                                    true
2673                                } else {
2674                                    g.group_name
2675                                        .to_lowercase()
2676                                        .contains(&self.iam_state.groups.filter.to_lowercase())
2677                                }
2678                            })
2679                            .collect();
2680                        if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
2681                            let arn = iam::format_arn(
2682                                &self.config.account_id,
2683                                "group",
2684                                &group.group_name,
2685                            );
2686                            copy_to_clipboard(&arn);
2687                        }
2688                    }
2689                } else if self.current_service == Service::SqsQueues {
2690                    if self.sqs_state.current_queue.is_some() {
2691                        // In queue detail view - copy queue ARN
2692                        if let Some(queue) = self
2693                            .sqs_state
2694                            .queues
2695                            .items
2696                            .iter()
2697                            .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
2698                        {
2699                            let arn = format!(
2700                                "arn:aws:sqs:{}:{}:{}",
2701                                crate::ui::sqs::extract_region(&queue.url),
2702                                crate::ui::sqs::extract_account_id(&queue.url),
2703                                queue.name
2704                            );
2705                            copy_to_clipboard(&arn);
2706                        }
2707                    } else {
2708                        // In list view - copy selected queue ARN
2709                        let filtered_queues = crate::ui::sqs::filtered_queues(
2710                            &self.sqs_state.queues.items,
2711                            &self.sqs_state.queues.filter,
2712                        );
2713                        if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
2714                            let arn = format!(
2715                                "arn:aws:sqs:{}:{}:{}",
2716                                crate::ui::sqs::extract_region(&queue.url),
2717                                crate::ui::sqs::extract_account_id(&queue.url),
2718                                queue.name
2719                            );
2720                            copy_to_clipboard(&arn);
2721                        }
2722                    }
2723                }
2724            }
2725            Action::CopyToClipboard => {
2726                // Request snapshot - will be captured after next render
2727                self.snapshot_requested = true;
2728            }
2729            Action::RetryLoad => {
2730                self.error_message = None;
2731                self.mode = Mode::Normal;
2732                self.log_groups_state.loading = true;
2733            }
2734            Action::ApplyFilter => {
2735                if self.mode == Mode::FilterInput
2736                    && self.current_service == Service::SqsQueues
2737                    && self.sqs_state.input_focus
2738                        == crate::common::InputFocus::Dropdown("SubscriptionRegion")
2739                {
2740                    let regions = crate::aws::Region::all();
2741                    if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
2742                        self.sqs_state.subscription_region_filter = region.code.to_string();
2743                    }
2744                    self.mode = Mode::Normal;
2745                } else if self.mode == Mode::InsightsInput {
2746                    use crate::app::InsightsFocus;
2747                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2748                        && self.insights_state.insights.show_dropdown
2749                    {
2750                        // Close dropdown, exit input mode, and execute query
2751                        self.insights_state.insights.show_dropdown = false;
2752                        self.mode = Mode::Normal;
2753                        if !self.insights_state.insights.selected_log_groups.is_empty() {
2754                            self.log_groups_state.loading = true;
2755                            self.insights_state.insights.query_completed = true;
2756                        }
2757                    }
2758                } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
2759                    if let Ok(page) = self.page_input.parse::<usize>() {
2760                        self.go_to_page(page);
2761                    }
2762                    self.page_input.clear();
2763                } else {
2764                    self.mode = Mode::Normal;
2765                    self.log_groups_state.filter_mode = false;
2766                }
2767            }
2768            Action::ToggleExactMatch => {
2769                if self.view_mode == ViewMode::Detail
2770                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2771                {
2772                    self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
2773                }
2774            }
2775            Action::ToggleShowExpired => {
2776                if self.view_mode == ViewMode::Detail
2777                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2778                {
2779                    self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
2780                }
2781            }
2782            Action::GoBack => {
2783                // ServicePicker: close if we have tabs
2784                if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
2785                    self.mode = Mode::Normal;
2786                    self.service_picker.filter.clear();
2787                }
2788                // S3: pop navigation stack first, then exit bucket
2789                else if self.current_service == Service::S3Buckets
2790                    && self.s3_state.current_bucket.is_some()
2791                {
2792                    if !self.s3_state.prefix_stack.is_empty() {
2793                        self.s3_state.prefix_stack.pop();
2794                        self.s3_state.buckets.loading = true;
2795                    } else {
2796                        self.s3_state.current_bucket = None;
2797                        self.s3_state.objects.clear();
2798                    }
2799                }
2800                // ECR: go back from images to repositories
2801                else if self.current_service == Service::EcrRepositories
2802                    && self.ecr_state.current_repository.is_some()
2803                {
2804                    if self.ecr_state.images.has_expanded_item() {
2805                        self.ecr_state.images.collapse();
2806                    } else {
2807                        self.ecr_state.current_repository = None;
2808                        self.ecr_state.current_repository_uri = None;
2809                        self.ecr_state.images.items.clear();
2810                        self.ecr_state.images.reset();
2811                    }
2812                }
2813                // SQS: go back from queue detail to list
2814                else if self.current_service == Service::SqsQueues
2815                    && self.sqs_state.current_queue.is_some()
2816                {
2817                    self.sqs_state.current_queue = None;
2818                }
2819                // IAM: go back from user detail to list
2820                else if self.current_service == Service::IamUsers
2821                    && self.iam_state.current_user.is_some()
2822                {
2823                    self.iam_state.current_user = None;
2824                    self.iam_state.policies.items.clear();
2825                    self.iam_state.policies.reset();
2826                    self.update_current_tab_breadcrumb();
2827                }
2828                // IAM: go back from group detail to list
2829                else if self.current_service == Service::IamUserGroups
2830                    && self.iam_state.current_group.is_some()
2831                {
2832                    self.iam_state.current_group = None;
2833                    self.update_current_tab_breadcrumb();
2834                }
2835                // IAM: go back from role detail to list
2836                else if self.current_service == Service::IamRoles {
2837                    if self.view_mode == ViewMode::PolicyView {
2838                        // Go back from policy view to role detail
2839                        self.view_mode = ViewMode::Detail;
2840                        self.iam_state.current_policy = None;
2841                        self.iam_state.policy_document.clear();
2842                        self.iam_state.policy_scroll = 0;
2843                        self.update_current_tab_breadcrumb();
2844                    } else if self.iam_state.current_role.is_some() {
2845                        self.iam_state.current_role = None;
2846                        self.iam_state.policies.items.clear();
2847                        self.iam_state.policies.reset();
2848                        self.update_current_tab_breadcrumb();
2849                    }
2850                }
2851                // Lambda: go back from version detail to function detail
2852                else if self.current_service == Service::LambdaFunctions
2853                    && self.lambda_state.current_version.is_some()
2854                {
2855                    self.lambda_state.current_version = None;
2856                    self.lambda_state.detail_tab = LambdaDetailTab::Versions;
2857                }
2858                // Lambda: go back from alias detail to function detail
2859                else if self.current_service == Service::LambdaFunctions
2860                    && self.lambda_state.current_alias.is_some()
2861                {
2862                    self.lambda_state.current_alias = None;
2863                    self.lambda_state.detail_tab = LambdaDetailTab::Aliases;
2864                }
2865                // Lambda: go back from function detail to list
2866                else if self.current_service == Service::LambdaFunctions
2867                    && self.lambda_state.current_function.is_some()
2868                {
2869                    self.lambda_state.current_function = None;
2870                    self.update_current_tab_breadcrumb();
2871                }
2872                // Lambda Applications: go back from application detail to list
2873                else if self.current_service == Service::LambdaApplications
2874                    && self.lambda_application_state.current_application.is_some()
2875                {
2876                    self.lambda_application_state.current_application = None;
2877                    self.update_current_tab_breadcrumb();
2878                }
2879                // CloudFormation: go back from stack detail to list
2880                else if self.current_service == Service::CloudFormationStacks
2881                    && self.cfn_state.current_stack.is_some()
2882                {
2883                    self.cfn_state.current_stack = None;
2884                    self.update_current_tab_breadcrumb();
2885                }
2886                // From insights results -> collapse if expanded, otherwise back to sidebar
2887                else if self.view_mode == ViewMode::InsightsResults {
2888                    if self.insights_state.insights.expanded_result.is_some() {
2889                        self.insights_state.insights.expanded_result = None;
2890                    }
2891                }
2892                // From alarms view -> collapse if expanded
2893                else if self.current_service == Service::CloudWatchAlarms {
2894                    if self.alarms_state.table.has_expanded_item() {
2895                        self.alarms_state.table.collapse();
2896                    }
2897                }
2898                // From events view -> collapse if expanded, otherwise back to detail view
2899                else if self.view_mode == ViewMode::Events {
2900                    if self.log_groups_state.expanded_event.is_some() {
2901                        self.log_groups_state.expanded_event = None;
2902                    } else {
2903                        self.view_mode = ViewMode::Detail;
2904                        self.log_groups_state.event_filter.clear();
2905                    }
2906                }
2907                // From detail view -> back to list view
2908                else if self.view_mode == ViewMode::Detail {
2909                    self.view_mode = ViewMode::List;
2910                    self.log_groups_state.stream_filter.clear();
2911                    self.log_groups_state.exact_match = false;
2912                    self.log_groups_state.show_expired = false;
2913                }
2914            }
2915            Action::OpenInConsole | Action::OpenInBrowser => {
2916                let url = self.get_console_url();
2917                let _ = webbrowser::open(&url);
2918            }
2919            Action::ShowHelp => {
2920                self.mode = Mode::HelpModal;
2921            }
2922            Action::OpenRegionPicker => {
2923                self.region_filter.clear();
2924                self.region_picker_selected = 0;
2925                self.measure_region_latencies();
2926                self.mode = Mode::RegionPicker;
2927            }
2928            Action::OpenProfilePicker => {
2929                self.profile_filter.clear();
2930                self.profile_picker_selected = 0;
2931                self.available_profiles = Self::load_aws_profiles();
2932                self.mode = Mode::ProfilePicker;
2933            }
2934            Action::OpenCalendar => {
2935                self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
2936                self.calendar_selecting = CalendarField::StartDate;
2937                self.mode = Mode::CalendarPicker;
2938            }
2939            Action::CloseCalendar => {
2940                self.mode = Mode::Normal;
2941                self.calendar_date = None;
2942            }
2943            Action::CalendarPrevDay => {
2944                if let Some(date) = self.calendar_date {
2945                    self.calendar_date = date.checked_sub(time::Duration::days(1));
2946                }
2947            }
2948            Action::CalendarNextDay => {
2949                if let Some(date) = self.calendar_date {
2950                    self.calendar_date = date.checked_add(time::Duration::days(1));
2951                }
2952            }
2953            Action::CalendarPrevWeek => {
2954                if let Some(date) = self.calendar_date {
2955                    self.calendar_date = date.checked_sub(time::Duration::weeks(1));
2956                }
2957            }
2958            Action::CalendarNextWeek => {
2959                if let Some(date) = self.calendar_date {
2960                    self.calendar_date = date.checked_add(time::Duration::weeks(1));
2961                }
2962            }
2963            Action::CalendarPrevMonth => {
2964                if let Some(date) = self.calendar_date {
2965                    self.calendar_date = Some(if date.month() == time::Month::January {
2966                        date.replace_month(time::Month::December)
2967                            .unwrap()
2968                            .replace_year(date.year() - 1)
2969                            .unwrap()
2970                    } else {
2971                        date.replace_month(date.month().previous()).unwrap()
2972                    });
2973                }
2974            }
2975            Action::CalendarNextMonth => {
2976                if let Some(date) = self.calendar_date {
2977                    self.calendar_date = Some(if date.month() == time::Month::December {
2978                        date.replace_month(time::Month::January)
2979                            .unwrap()
2980                            .replace_year(date.year() + 1)
2981                            .unwrap()
2982                    } else {
2983                        date.replace_month(date.month().next()).unwrap()
2984                    });
2985                }
2986            }
2987            Action::CalendarSelect => {
2988                if let Some(date) = self.calendar_date {
2989                    let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
2990                        .unix_timestamp()
2991                        * 1000;
2992                    match self.calendar_selecting {
2993                        CalendarField::StartDate => {
2994                            self.log_groups_state.start_time = Some(timestamp);
2995                            self.calendar_selecting = CalendarField::EndDate;
2996                        }
2997                        CalendarField::EndDate => {
2998                            self.log_groups_state.end_time = Some(timestamp);
2999                            self.mode = Mode::Normal;
3000                            self.calendar_date = None;
3001                        }
3002                    }
3003                }
3004            }
3005        }
3006    }
3007
3008    pub fn filtered_services(&self) -> Vec<&'static str> {
3009        let mut services = if self.service_picker.filter.is_empty() {
3010            self.service_picker.services.clone()
3011        } else {
3012            self.service_picker
3013                .services
3014                .iter()
3015                .filter(|s| {
3016                    s.to_lowercase()
3017                        .contains(&self.service_picker.filter.to_lowercase())
3018                })
3019                .copied()
3020                .collect()
3021        };
3022        services.sort();
3023        services
3024    }
3025
3026    pub fn selected_log_group(&self) -> Option<&LogGroup> {
3027        crate::ui::cw::logs::selected_log_group(self)
3028    }
3029
3030    pub fn filtered_log_streams(&self) -> Vec<&LogStream> {
3031        crate::ui::cw::logs::filtered_log_streams(self)
3032    }
3033
3034    pub fn filtered_log_events(&self) -> Vec<&LogEvent> {
3035        crate::ui::cw::logs::filtered_log_events(self)
3036    }
3037
3038    pub fn filtered_log_groups(&self) -> Vec<&LogGroup> {
3039        crate::ui::cw::logs::filtered_log_groups(self)
3040    }
3041
3042    pub fn filtered_ecr_repositories(&self) -> Vec<&EcrRepository> {
3043        crate::ui::ecr::filtered_ecr_repositories(self)
3044    }
3045
3046    pub fn filtered_ecr_images(&self) -> Vec<&EcrImage> {
3047        crate::ui::ecr::filtered_ecr_images(self)
3048    }
3049
3050    pub fn filtered_cloudformation_stacks(&self) -> Vec<&CfnStack> {
3051        crate::ui::cfn::filtered_cloudformation_stacks(self)
3052    }
3053
3054    pub fn breadcrumbs(&self) -> String {
3055        if !self.service_selected {
3056            return String::new();
3057        }
3058
3059        let mut parts = vec![];
3060
3061        match self.current_service {
3062            Service::CloudWatchLogGroups => {
3063                parts.push("CloudWatch".to_string());
3064                parts.push("Log groups".to_string());
3065
3066                if self.view_mode != ViewMode::List {
3067                    if let Some(group) = self.selected_log_group() {
3068                        parts.push(group.name.clone());
3069                    }
3070                }
3071
3072                if self.view_mode == ViewMode::Events {
3073                    if let Some(stream) = self
3074                        .log_groups_state
3075                        .log_streams
3076                        .get(self.log_groups_state.selected_stream)
3077                    {
3078                        parts.push(stream.name.clone());
3079                    }
3080                }
3081            }
3082            Service::CloudWatchInsights => {
3083                parts.push("CloudWatch".to_string());
3084                parts.push("Insights".to_string());
3085            }
3086            Service::CloudWatchAlarms => {
3087                parts.push("CloudWatch".to_string());
3088                parts.push("Alarms".to_string());
3089            }
3090            Service::S3Buckets => {
3091                parts.push("S3".to_string());
3092                if let Some(bucket) = &self.s3_state.current_bucket {
3093                    parts.push(bucket.clone());
3094                    if let Some(prefix) = self.s3_state.prefix_stack.last() {
3095                        parts.push(prefix.trim_end_matches('/').to_string());
3096                    }
3097                } else {
3098                    parts.push("Buckets".to_string());
3099                }
3100            }
3101            Service::SqsQueues => {
3102                parts.push("SQS".to_string());
3103                parts.push("Queues".to_string());
3104            }
3105            Service::EcrRepositories => {
3106                parts.push("ECR".to_string());
3107                if let Some(repo) = &self.ecr_state.current_repository {
3108                    parts.push(repo.clone());
3109                } else {
3110                    parts.push("Repositories".to_string());
3111                }
3112            }
3113            Service::LambdaFunctions => {
3114                parts.push("Lambda".to_string());
3115                if let Some(func) = &self.lambda_state.current_function {
3116                    parts.push(func.clone());
3117                } else {
3118                    parts.push("Functions".to_string());
3119                }
3120            }
3121            Service::LambdaApplications => {
3122                parts.push("Lambda".to_string());
3123                parts.push("Applications".to_string());
3124            }
3125            Service::CloudFormationStacks => {
3126                parts.push("CloudFormation".to_string());
3127                if let Some(stack_name) = &self.cfn_state.current_stack {
3128                    parts.push(stack_name.clone());
3129                } else {
3130                    parts.push("Stacks".to_string());
3131                }
3132            }
3133            Service::IamUsers => {
3134                parts.push("IAM".to_string());
3135                parts.push("Users".to_string());
3136            }
3137            Service::IamRoles => {
3138                parts.push("IAM".to_string());
3139                parts.push("Roles".to_string());
3140                if let Some(role_name) = &self.iam_state.current_role {
3141                    parts.push(role_name.clone());
3142                    if let Some(policy_name) = &self.iam_state.current_policy {
3143                        parts.push(policy_name.clone());
3144                    }
3145                }
3146            }
3147            Service::IamUserGroups => {
3148                parts.push("IAM".to_string());
3149                parts.push("User Groups".to_string());
3150                if let Some(group_name) = &self.iam_state.current_group {
3151                    parts.push(group_name.clone());
3152                }
3153            }
3154        }
3155
3156        parts.join(" > ")
3157    }
3158
3159    pub fn update_current_tab_breadcrumb(&mut self) {
3160        if !self.tabs.is_empty() {
3161            self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
3162        }
3163    }
3164
3165    pub fn get_console_url(&self) -> String {
3166        use crate::{cfn, cw, ecr, iam, lambda, s3};
3167
3168        match self.current_service {
3169            Service::CloudWatchLogGroups => {
3170                if self.view_mode == ViewMode::Events {
3171                    if let Some(group) = self.selected_log_group() {
3172                        if let Some(stream) = self
3173                            .log_groups_state
3174                            .log_streams
3175                            .get(self.log_groups_state.selected_stream)
3176                        {
3177                            return cw::logs::console_url_stream(
3178                                &self.config.region,
3179                                &group.name,
3180                                &stream.name,
3181                            );
3182                        }
3183                    }
3184                } else if self.view_mode == ViewMode::Detail {
3185                    if let Some(group) = self.selected_log_group() {
3186                        return cw::logs::console_url_detail(&self.config.region, &group.name);
3187                    }
3188                }
3189                cw::logs::console_url_list(&self.config.region)
3190            }
3191            Service::CloudWatchInsights => cw::insights::console_url(
3192                &self.config.region,
3193                &self.config.account_id,
3194                &self.insights_state.insights.query_text,
3195                &self.insights_state.insights.selected_log_groups,
3196            ),
3197            Service::CloudWatchAlarms => {
3198                let view_type = match self.alarms_state.view_as {
3199                    AlarmViewMode::Table | AlarmViewMode::Detail => "table",
3200                    AlarmViewMode::Cards => "card",
3201                };
3202                cw::alarms::console_url(
3203                    &self.config.region,
3204                    view_type,
3205                    self.alarms_state.table.page_size.value(),
3206                    &self.alarms_state.sort_column,
3207                    self.alarms_state.sort_direction.as_str(),
3208                )
3209            }
3210            Service::S3Buckets => {
3211                if let Some(bucket_name) = &self.s3_state.current_bucket {
3212                    let prefix = self.s3_state.prefix_stack.join("");
3213                    s3::console_url_bucket(&self.config.region, bucket_name, &prefix)
3214                } else {
3215                    s3::console_url_buckets(&self.config.region)
3216                }
3217            }
3218            Service::SqsQueues => {
3219                if let Some(queue_url) = &self.sqs_state.current_queue {
3220                    crate::sqs::console_url_queue_detail(&self.config.region, queue_url)
3221                } else {
3222                    crate::sqs::console_url_queues(&self.config.region)
3223                }
3224            }
3225            Service::EcrRepositories => {
3226                if let Some(repo_name) = &self.ecr_state.current_repository {
3227                    ecr::console_url_private_repository(
3228                        &self.config.region,
3229                        &self.config.account_id,
3230                        repo_name,
3231                    )
3232                } else {
3233                    ecr::console_url_repositories(&self.config.region)
3234                }
3235            }
3236            Service::LambdaFunctions => {
3237                if let Some(func_name) = &self.lambda_state.current_function {
3238                    if let Some(version) = &self.lambda_state.current_version {
3239                        lambda::console_url_function_version(
3240                            &self.config.region,
3241                            func_name,
3242                            version,
3243                            &self.lambda_state.detail_tab,
3244                        )
3245                    } else {
3246                        lambda::console_url_function_detail(&self.config.region, func_name)
3247                    }
3248                } else {
3249                    lambda::console_url_functions(&self.config.region)
3250                }
3251            }
3252            Service::LambdaApplications => {
3253                if let Some(app_name) = &self.lambda_application_state.current_application {
3254                    lambda::console_url_application_detail(
3255                        &self.config.region,
3256                        app_name,
3257                        &self.lambda_application_state.detail_tab,
3258                    )
3259                } else {
3260                    lambda::console_url_applications(&self.config.region)
3261                }
3262            }
3263            Service::CloudFormationStacks => {
3264                if let Some(stack_name) = &self.cfn_state.current_stack {
3265                    if let Some(stack) = self
3266                        .cfn_state
3267                        .table
3268                        .items
3269                        .iter()
3270                        .find(|s| &s.name == stack_name)
3271                    {
3272                        return cfn::console_url_stack_detail_with_tab(
3273                            &self.config.region,
3274                            &stack.stack_id,
3275                            &self.cfn_state.detail_tab,
3276                        );
3277                    }
3278                }
3279                cfn::console_url_stacks(&self.config.region)
3280            }
3281            Service::IamUsers => {
3282                if let Some(user_name) = &self.iam_state.current_user {
3283                    let section = match self.iam_state.user_tab {
3284                        UserTab::Permissions => "permissions",
3285                        UserTab::Groups => "groups",
3286                        UserTab::Tags => "tags",
3287                        UserTab::SecurityCredentials => "security_credentials",
3288                        UserTab::LastAccessed => "access_advisor",
3289                    };
3290                    iam::console_url_user_detail(&self.config.region, user_name, section)
3291                } else {
3292                    iam::console_url_users(&self.config.region)
3293                }
3294            }
3295            Service::IamRoles => {
3296                if let Some(policy_name) = &self.iam_state.current_policy {
3297                    if let Some(role_name) = &self.iam_state.current_role {
3298                        return iam::console_url_role_policy(
3299                            &self.config.region,
3300                            role_name,
3301                            policy_name,
3302                        );
3303                    }
3304                }
3305                if let Some(role_name) = &self.iam_state.current_role {
3306                    let section = match self.iam_state.role_tab {
3307                        RoleTab::Permissions => "permissions",
3308                        RoleTab::TrustRelationships => "trust_relationships",
3309                        RoleTab::Tags => "tags",
3310                        RoleTab::LastAccessed => "access_advisor",
3311                        RoleTab::RevokeSessions => "revoke_sessions",
3312                    };
3313                    iam::console_url_role_detail(&self.config.region, role_name, section)
3314                } else {
3315                    iam::console_url_roles(&self.config.region)
3316                }
3317            }
3318            Service::IamUserGroups => iam::console_url_groups(&self.config.region),
3319        }
3320    }
3321
3322    fn calculate_total_bucket_rows(&self) -> usize {
3323        crate::ui::s3::calculate_total_bucket_rows(self)
3324    }
3325
3326    fn calculate_total_object_rows(&self) -> usize {
3327        crate::ui::s3::calculate_total_object_rows(self)
3328    }
3329
3330    fn next_item(&mut self) {
3331        match self.mode {
3332            Mode::FilterInput => {
3333                if self.current_service == Service::CloudFormationStacks {
3334                    use crate::ui::cfn::STATUS_FILTER;
3335                    if self.cfn_state.input_focus == STATUS_FILTER {
3336                        self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3337                    }
3338                } else if self.current_service == Service::SqsQueues {
3339                    use crate::ui::sqs::SUBSCRIPTION_REGION;
3340                    if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
3341                        let regions = crate::aws::Region::all();
3342                        self.sqs_state.subscription_region_selected =
3343                            (self.sqs_state.subscription_region_selected + 1)
3344                                .min(regions.len() - 1);
3345                    }
3346                }
3347            }
3348            Mode::RegionPicker => {
3349                let filtered = self.get_filtered_regions();
3350                if !filtered.is_empty() {
3351                    self.region_picker_selected =
3352                        (self.region_picker_selected + 1).min(filtered.len() - 1);
3353                }
3354            }
3355            Mode::ProfilePicker => {
3356                let filtered = self.get_filtered_profiles();
3357                if !filtered.is_empty() {
3358                    self.profile_picker_selected =
3359                        (self.profile_picker_selected + 1).min(filtered.len() - 1);
3360                }
3361            }
3362            Mode::SessionPicker => {
3363                let filtered = self.get_filtered_sessions();
3364                if !filtered.is_empty() {
3365                    self.session_picker_selected =
3366                        (self.session_picker_selected + 1).min(filtered.len() - 1);
3367                }
3368            }
3369            Mode::InsightsInput => {
3370                use crate::app::InsightsFocus;
3371                if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3372                    && self.insights_state.insights.show_dropdown
3373                    && !self.insights_state.insights.log_group_matches.is_empty()
3374                {
3375                    let max = self.insights_state.insights.log_group_matches.len() - 1;
3376                    self.insights_state.insights.dropdown_selected =
3377                        (self.insights_state.insights.dropdown_selected + 1).min(max);
3378                }
3379            }
3380            Mode::ColumnSelector => {
3381                let max = if self.current_service == Service::S3Buckets
3382                    && self.s3_state.current_bucket.is_none()
3383                {
3384                    self.s3_bucket_column_ids.len() - 1
3385                } else if self.view_mode == ViewMode::Events {
3386                    self.cw_log_event_column_ids.len() - 1
3387                } else if self.view_mode == ViewMode::Detail {
3388                    self.cw_log_stream_column_ids.len() - 1
3389                } else if self.current_service == Service::CloudWatchAlarms {
3390                    // 16 columns + 1 header + 1 empty + 2 view + 1 header + 1 empty + 4 page + 1 header + 1 empty + 1 wrap + 1 header = 30
3391                    29
3392                } else if self.current_service == Service::EcrRepositories {
3393                    if self.ecr_state.current_repository.is_some() {
3394                        // Images: N columns + 1 header + 1 empty + 1 header + 4 page sizes = N + 7
3395                        self.ecr_image_column_ids.len() + 6
3396                    } else {
3397                        // Repositories: just columns
3398                        self.ecr_repo_column_ids.len() - 1
3399                    }
3400                } else if self.current_service == Service::SqsQueues {
3401                    self.sqs_column_ids.len() - 1
3402                } else if self.current_service == Service::LambdaFunctions {
3403                    // Lambda: N columns + 1 header + 1 empty + 1 header + 4 page sizes = N + 7
3404                    self.lambda_state.function_column_ids.len() + 6
3405                } else if self.current_service == Service::LambdaApplications {
3406                    // Lambda Applications: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3407                    self.lambda_application_column_ids.len() + 5
3408                } else if self.current_service == Service::CloudFormationStacks {
3409                    // CloudFormation: N columns + 1 header + 1 empty + 1 header + 4 page sizes = N + 7
3410                    self.cfn_column_ids.len() + 6
3411                } else if self.current_service == Service::IamUsers {
3412                    if self.iam_state.current_user.is_some() {
3413                        // Policy columns: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3414                        self.iam_policy_column_ids.len() + 5
3415                    } else {
3416                        // User columns: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3417                        self.iam_user_column_ids.len() + 5
3418                    }
3419                } else if self.current_service == Service::IamRoles {
3420                    if self.iam_state.current_role.is_some() {
3421                        // Policy columns: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3422                        self.iam_policy_column_ids.len() + 5
3423                    } else {
3424                        // Role columns: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3425                        self.iam_role_column_ids.len() + 5
3426                    }
3427                } else {
3428                    self.cw_log_group_column_ids.len() - 1
3429                };
3430                self.column_selector_index = (self.column_selector_index + 1).min(max);
3431            }
3432            Mode::ServicePicker => {
3433                let filtered = self.filtered_services();
3434                if !filtered.is_empty() {
3435                    self.service_picker.selected =
3436                        (self.service_picker.selected + 1).min(filtered.len() - 1);
3437                }
3438            }
3439            Mode::TabPicker => {
3440                let filtered = self.get_filtered_tabs();
3441                if !filtered.is_empty() {
3442                    self.tab_picker_selected =
3443                        (self.tab_picker_selected + 1).min(filtered.len() - 1);
3444                }
3445            }
3446            Mode::Normal => {
3447                if !self.service_selected {
3448                    let filtered = self.filtered_services();
3449                    if !filtered.is_empty() {
3450                        self.service_picker.selected =
3451                            (self.service_picker.selected + 1).min(filtered.len() - 1);
3452                    }
3453                } else if self.current_service == Service::S3Buckets {
3454                    if self.s3_state.current_bucket.is_some() {
3455                        if self.s3_state.object_tab == S3ObjectTab::Properties {
3456                            // Scroll properties view
3457                            self.s3_state.properties_scroll =
3458                                self.s3_state.properties_scroll.saturating_add(1);
3459                        } else {
3460                            // Calculate total rows including all nested preview items
3461                            let total_rows = self.calculate_total_object_rows();
3462                            let max = total_rows.saturating_sub(1);
3463                            self.s3_state.selected_object =
3464                                (self.s3_state.selected_object + 1).min(max);
3465
3466                            // Adjust scroll offset if selection goes below viewport
3467                            let visible_rows = self.s3_state.object_visible_rows.get();
3468                            if self.s3_state.selected_object
3469                                >= self.s3_state.object_scroll_offset + visible_rows
3470                            {
3471                                self.s3_state.object_scroll_offset =
3472                                    self.s3_state.selected_object - visible_rows + 1;
3473                            }
3474                        }
3475                    } else {
3476                        // Navigate rows in bucket list
3477                        let total_rows = self.calculate_total_bucket_rows();
3478                        if total_rows > 0 {
3479                            self.s3_state.selected_row =
3480                                (self.s3_state.selected_row + 1).min(total_rows - 1);
3481
3482                            // Adjust scroll offset if selection goes below viewport
3483                            let visible_rows = self.s3_state.bucket_visible_rows.get();
3484                            if self.s3_state.selected_row
3485                                >= self.s3_state.bucket_scroll_offset + visible_rows
3486                            {
3487                                self.s3_state.bucket_scroll_offset =
3488                                    self.s3_state.selected_row - visible_rows + 1;
3489                            }
3490                        }
3491                    }
3492                } else if self.view_mode == ViewMode::InsightsResults {
3493                    let max = self
3494                        .insights_state
3495                        .insights
3496                        .query_results
3497                        .len()
3498                        .saturating_sub(1);
3499                    if self.insights_state.insights.results_selected < max {
3500                        self.insights_state.insights.results_selected += 1;
3501                    }
3502                } else if self.view_mode == ViewMode::PolicyView {
3503                    let lines = self.iam_state.policy_document.lines().count();
3504                    let max_scroll = lines.saturating_sub(1);
3505                    self.iam_state.policy_scroll =
3506                        (self.iam_state.policy_scroll + 1).min(max_scroll);
3507                } else if self.current_service == Service::SqsQueues
3508                    && self.sqs_state.current_queue.is_some()
3509                    && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3510                {
3511                    let lines = self.sqs_state.policy_document.lines().count();
3512                    let max_scroll = lines.saturating_sub(1);
3513                    self.sqs_state.policy_scroll =
3514                        (self.sqs_state.policy_scroll + 1).min(max_scroll);
3515                } else if self.current_service == Service::SqsQueues
3516                    && self.sqs_state.current_queue.is_some()
3517                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3518                {
3519                    self.sqs_state.monitoring_scroll =
3520                        (self.sqs_state.monitoring_scroll + 1).min(8);
3521                } else if self.view_mode == ViewMode::Events {
3522                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3523                    if self.log_groups_state.event_scroll_offset >= max_scroll {
3524                        // At the end, do nothing
3525                    } else {
3526                        self.log_groups_state.event_scroll_offset =
3527                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3528                    }
3529                } else if self.current_service == Service::CloudWatchLogGroups {
3530                    if self.view_mode == ViewMode::List {
3531                        let filtered_groups = self.filtered_log_groups();
3532                        self.log_groups_state
3533                            .log_groups
3534                            .next_item(filtered_groups.len());
3535                    } else if self.view_mode == ViewMode::Detail {
3536                        let filtered_streams = self.filtered_log_streams();
3537                        if !filtered_streams.is_empty() {
3538                            let max = filtered_streams.len() - 1;
3539                            if self.log_groups_state.selected_stream >= max {
3540                                // At the end, do nothing
3541                            } else {
3542                                self.log_groups_state.selected_stream =
3543                                    (self.log_groups_state.selected_stream + 1).min(max);
3544                            }
3545                        }
3546                    }
3547                } else if self.current_service == Service::CloudWatchAlarms {
3548                    let filtered_alarms = match self.alarms_state.alarm_tab {
3549                        AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
3550                        AlarmTab::InAlarm => self
3551                            .alarms_state
3552                            .table
3553                            .items
3554                            .iter()
3555                            .filter(|a| a.state.to_uppercase() == "ALARM")
3556                            .count(),
3557                    };
3558                    if filtered_alarms > 0 {
3559                        self.alarms_state.table.next_item(filtered_alarms);
3560                    }
3561                } else if self.current_service == Service::EcrRepositories {
3562                    if self.ecr_state.current_repository.is_some() {
3563                        let filtered_images = self.filtered_ecr_images();
3564                        if !filtered_images.is_empty() {
3565                            self.ecr_state.images.next_item(filtered_images.len());
3566                        }
3567                    } else {
3568                        let filtered_repos = self.filtered_ecr_repositories();
3569                        if !filtered_repos.is_empty() {
3570                            self.ecr_state.repositories.selected =
3571                                (self.ecr_state.repositories.selected + 1)
3572                                    .min(filtered_repos.len() - 1);
3573                            self.ecr_state.repositories.snap_to_page();
3574                        }
3575                    }
3576                } else if self.current_service == Service::SqsQueues {
3577                    if self.sqs_state.current_queue.is_some()
3578                        && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
3579                    {
3580                        let filtered = crate::ui::sqs::filtered_lambda_triggers(self);
3581                        if !filtered.is_empty() {
3582                            self.sqs_state.triggers.next_item(filtered.len());
3583                        }
3584                    } else if self.sqs_state.current_queue.is_some()
3585                        && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
3586                    {
3587                        let filtered = crate::ui::sqs::filtered_eventbridge_pipes(self);
3588                        if !filtered.is_empty() {
3589                            self.sqs_state.pipes.next_item(filtered.len());
3590                        }
3591                    } else if self.sqs_state.current_queue.is_some()
3592                        && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
3593                    {
3594                        let filtered = crate::ui::sqs::filtered_tags(self);
3595                        if !filtered.is_empty() {
3596                            self.sqs_state.tags.next_item(filtered.len());
3597                        }
3598                    } else if self.sqs_state.current_queue.is_some()
3599                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3600                    {
3601                        let filtered = crate::ui::sqs::filtered_subscriptions(self);
3602                        if !filtered.is_empty() {
3603                            self.sqs_state.subscriptions.next_item(filtered.len());
3604                        }
3605                    } else {
3606                        let filtered_queues = crate::ui::sqs::filtered_queues(
3607                            &self.sqs_state.queues.items,
3608                            &self.sqs_state.queues.filter,
3609                        );
3610                        if !filtered_queues.is_empty() {
3611                            self.sqs_state.queues.next_item(filtered_queues.len());
3612                        }
3613                    }
3614                } else if self.current_service == Service::LambdaFunctions {
3615                    if self.lambda_state.current_function.is_some()
3616                        && self.lambda_state.detail_tab == LambdaDetailTab::Code
3617                    {
3618                        // Layer table navigation in Code tab
3619                        if let Some(func_name) = &self.lambda_state.current_function {
3620                            if let Some(func) = self
3621                                .lambda_state
3622                                .table
3623                                .items
3624                                .iter()
3625                                .find(|f| f.name == *func_name)
3626                            {
3627                                let max = func.layers.len().saturating_sub(1);
3628                                if !func.layers.is_empty() {
3629                                    self.lambda_state.layer_selected =
3630                                        (self.lambda_state.layer_selected + 1).min(max);
3631                                }
3632                            }
3633                        }
3634                    } else if self.lambda_state.current_function.is_some()
3635                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3636                    {
3637                        // Version table navigation
3638                        let filtered: Vec<_> = self
3639                            .lambda_state
3640                            .version_table
3641                            .items
3642                            .iter()
3643                            .filter(|v| {
3644                                self.lambda_state.version_table.filter.is_empty()
3645                                    || v.version.to_lowercase().contains(
3646                                        &self.lambda_state.version_table.filter.to_lowercase(),
3647                                    )
3648                                    || v.aliases.to_lowercase().contains(
3649                                        &self.lambda_state.version_table.filter.to_lowercase(),
3650                                    )
3651                                    || v.description.to_lowercase().contains(
3652                                        &self.lambda_state.version_table.filter.to_lowercase(),
3653                                    )
3654                            })
3655                            .collect();
3656                        if !filtered.is_empty() {
3657                            self.lambda_state.version_table.selected =
3658                                (self.lambda_state.version_table.selected + 1)
3659                                    .min(filtered.len() - 1);
3660                            self.lambda_state.version_table.snap_to_page();
3661                        }
3662                    } else if self.lambda_state.current_function.is_some()
3663                        && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3664                            || (self.lambda_state.current_version.is_some()
3665                                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3666                    {
3667                        // Alias table navigation (both in Aliases tab and Version Configuration)
3668                        let version_filter = self.lambda_state.current_version.clone();
3669                        let filtered: Vec<_> = self
3670                            .lambda_state
3671                            .alias_table
3672                            .items
3673                            .iter()
3674                            .filter(|a| {
3675                                (version_filter.is_none()
3676                                    || a.versions.contains(version_filter.as_ref().unwrap()))
3677                                    && (self.lambda_state.alias_table.filter.is_empty()
3678                                        || a.name.to_lowercase().contains(
3679                                            &self.lambda_state.alias_table.filter.to_lowercase(),
3680                                        )
3681                                        || a.versions.to_lowercase().contains(
3682                                            &self.lambda_state.alias_table.filter.to_lowercase(),
3683                                        )
3684                                        || a.description.to_lowercase().contains(
3685                                            &self.lambda_state.alias_table.filter.to_lowercase(),
3686                                        ))
3687                            })
3688                            .collect();
3689                        if !filtered.is_empty() {
3690                            self.lambda_state.alias_table.selected =
3691                                (self.lambda_state.alias_table.selected + 1)
3692                                    .min(filtered.len() - 1);
3693                            self.lambda_state.alias_table.snap_to_page();
3694                        }
3695                    } else if self.lambda_state.current_function.is_none() {
3696                        let filtered = crate::ui::lambda::filtered_lambda_functions(self);
3697                        if !filtered.is_empty() {
3698                            self.lambda_state.table.next_item(filtered.len());
3699                            self.lambda_state.table.snap_to_page();
3700                        }
3701                    }
3702                } else if self.current_service == Service::LambdaApplications {
3703                    if self.lambda_application_state.current_application.is_some() {
3704                        if self.lambda_application_state.detail_tab
3705                            == LambdaApplicationDetailTab::Overview
3706                        {
3707                            let len = self.lambda_application_state.resources.items.len();
3708                            if len > 0 {
3709                                self.lambda_application_state.resources.next_item(len);
3710                            }
3711                        } else {
3712                            let len = self.lambda_application_state.deployments.items.len();
3713                            if len > 0 {
3714                                self.lambda_application_state.deployments.next_item(len);
3715                            }
3716                        }
3717                    } else {
3718                        let filtered = crate::ui::lambda::filtered_lambda_applications(self);
3719                        if !filtered.is_empty() {
3720                            self.lambda_application_state.table.selected =
3721                                (self.lambda_application_state.table.selected + 1)
3722                                    .min(filtered.len() - 1);
3723                            self.lambda_application_state.table.snap_to_page();
3724                        }
3725                    }
3726                } else if self.current_service == Service::CloudFormationStacks {
3727                    let filtered = self.filtered_cloudformation_stacks();
3728                    self.cfn_state.table.next_item(filtered.len());
3729                } else if self.current_service == Service::IamUsers {
3730                    if self.iam_state.current_user.is_some() {
3731                        if self.iam_state.user_tab == UserTab::Tags {
3732                            let filtered = crate::ui::iam::filtered_user_tags(self);
3733                            if !filtered.is_empty() {
3734                                self.iam_state.user_tags.next_item(filtered.len());
3735                            }
3736                        } else {
3737                            let filtered = crate::ui::iam::filtered_iam_policies(self);
3738                            if !filtered.is_empty() {
3739                                self.iam_state.policies.next_item(filtered.len());
3740                            }
3741                        }
3742                    } else {
3743                        let filtered = crate::ui::iam::filtered_iam_users(self);
3744                        if !filtered.is_empty() {
3745                            self.iam_state.users.next_item(filtered.len());
3746                        }
3747                    }
3748                } else if self.current_service == Service::IamRoles {
3749                    if self.iam_state.current_role.is_some() {
3750                        if self.iam_state.role_tab == RoleTab::TrustRelationships {
3751                            let lines = self.iam_state.trust_policy_document.lines().count();
3752                            let max_scroll = lines.saturating_sub(1);
3753                            self.iam_state.trust_policy_scroll =
3754                                (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
3755                        } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
3756                            self.iam_state.revoke_sessions_scroll =
3757                                (self.iam_state.revoke_sessions_scroll + 1).min(19);
3758                        } else if self.iam_state.role_tab == RoleTab::Tags {
3759                            let filtered = crate::ui::iam::filtered_tags(self);
3760                            if !filtered.is_empty() {
3761                                self.iam_state.tags.next_item(filtered.len());
3762                            }
3763                        } else if self.iam_state.role_tab == RoleTab::LastAccessed {
3764                            let filtered = crate::ui::iam::filtered_last_accessed(self);
3765                            if !filtered.is_empty() {
3766                                self.iam_state
3767                                    .last_accessed_services
3768                                    .next_item(filtered.len());
3769                            }
3770                        } else {
3771                            let filtered = crate::ui::iam::filtered_iam_policies(self);
3772                            if !filtered.is_empty() {
3773                                self.iam_state.policies.next_item(filtered.len());
3774                            }
3775                        }
3776                    } else {
3777                        let filtered = crate::ui::iam::filtered_iam_roles(self);
3778                        if !filtered.is_empty() {
3779                            self.iam_state.roles.next_item(filtered.len());
3780                        }
3781                    }
3782                } else if self.current_service == Service::IamUserGroups {
3783                    if self.iam_state.current_group.is_some() {
3784                        if self.iam_state.group_tab == GroupTab::Users {
3785                            let filtered: Vec<_> = self
3786                                .iam_state
3787                                .group_users
3788                                .items
3789                                .iter()
3790                                .filter(|u| {
3791                                    if self.iam_state.group_users.filter.is_empty() {
3792                                        true
3793                                    } else {
3794                                        u.user_name.to_lowercase().contains(
3795                                            &self.iam_state.group_users.filter.to_lowercase(),
3796                                        )
3797                                    }
3798                                })
3799                                .collect();
3800                            if !filtered.is_empty() {
3801                                self.iam_state.group_users.next_item(filtered.len());
3802                            }
3803                        } else if self.iam_state.group_tab == GroupTab::Permissions {
3804                            let filtered = crate::ui::iam::filtered_iam_policies(self);
3805                            if !filtered.is_empty() {
3806                                self.iam_state.policies.next_item(filtered.len());
3807                            }
3808                        } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
3809                            let filtered = crate::ui::iam::filtered_last_accessed(self);
3810                            if !filtered.is_empty() {
3811                                self.iam_state
3812                                    .last_accessed_services
3813                                    .next_item(filtered.len());
3814                            }
3815                        }
3816                    } else {
3817                        let filtered: Vec<_> = self
3818                            .iam_state
3819                            .groups
3820                            .items
3821                            .iter()
3822                            .filter(|g| {
3823                                if self.iam_state.groups.filter.is_empty() {
3824                                    true
3825                                } else {
3826                                    g.group_name
3827                                        .to_lowercase()
3828                                        .contains(&self.iam_state.groups.filter.to_lowercase())
3829                                }
3830                            })
3831                            .collect();
3832                        if !filtered.is_empty() {
3833                            self.iam_state.groups.next_item(filtered.len());
3834                        }
3835                    }
3836                }
3837            }
3838            _ => {}
3839        }
3840    }
3841
3842    fn prev_item(&mut self) {
3843        match self.mode {
3844            Mode::FilterInput => {
3845                if self.current_service == Service::CloudFormationStacks {
3846                    use crate::ui::cfn::STATUS_FILTER;
3847                    if self.cfn_state.input_focus == STATUS_FILTER {
3848                        self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
3849                    }
3850                } else if self.current_service == Service::SqsQueues {
3851                    use crate::ui::sqs::SUBSCRIPTION_REGION;
3852                    if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
3853                        self.sqs_state.subscription_region_selected = self
3854                            .sqs_state
3855                            .subscription_region_selected
3856                            .saturating_sub(1);
3857                    }
3858                }
3859            }
3860            Mode::RegionPicker => {
3861                self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
3862            }
3863            Mode::ProfilePicker => {
3864                self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
3865            }
3866            Mode::SessionPicker => {
3867                self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
3868            }
3869            Mode::InsightsInput => {
3870                use crate::app::InsightsFocus;
3871                if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3872                    && self.insights_state.insights.show_dropdown
3873                    && !self.insights_state.insights.log_group_matches.is_empty()
3874                {
3875                    self.insights_state.insights.dropdown_selected = self
3876                        .insights_state
3877                        .insights
3878                        .dropdown_selected
3879                        .saturating_sub(1);
3880                }
3881            }
3882            Mode::ColumnSelector => {
3883                self.column_selector_index = self.column_selector_index.saturating_sub(1);
3884            }
3885            Mode::ServicePicker => {
3886                self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3887            }
3888            Mode::TabPicker => {
3889                self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
3890            }
3891            Mode::Normal => {
3892                if !self.service_selected {
3893                    self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3894                } else if self.current_service == Service::S3Buckets {
3895                    if self.s3_state.current_bucket.is_some() {
3896                        if self.s3_state.object_tab == S3ObjectTab::Properties {
3897                            self.s3_state.properties_scroll =
3898                                self.s3_state.properties_scroll.saturating_sub(1);
3899                        } else {
3900                            self.s3_state.selected_object =
3901                                self.s3_state.selected_object.saturating_sub(1);
3902
3903                            // Adjust scroll offset if selection goes above viewport
3904                            if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
3905                                self.s3_state.object_scroll_offset = self.s3_state.selected_object;
3906                            }
3907                        }
3908                    } else {
3909                        self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
3910
3911                        // Adjust scroll offset if selection goes above viewport
3912                        if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
3913                            self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
3914                        }
3915                    }
3916                } else if self.view_mode == ViewMode::InsightsResults {
3917                    if self.insights_state.insights.results_selected > 0 {
3918                        self.insights_state.insights.results_selected -= 1;
3919                    }
3920                } else if self.view_mode == ViewMode::PolicyView {
3921                    self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
3922                } else if self.current_service == Service::SqsQueues
3923                    && self.sqs_state.current_queue.is_some()
3924                    && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3925                {
3926                    self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(1);
3927                } else if self.current_service == Service::SqsQueues
3928                    && self.sqs_state.current_queue.is_some()
3929                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3930                {
3931                    self.sqs_state.monitoring_scroll =
3932                        self.sqs_state.monitoring_scroll.saturating_sub(1);
3933                } else if self.view_mode == ViewMode::Events {
3934                    if self.log_groups_state.event_scroll_offset == 0 {
3935                        if self.log_groups_state.has_older_events {
3936                            self.log_groups_state.loading = true;
3937                        }
3938                        // Don't move if at position 0
3939                    } else {
3940                        self.log_groups_state.event_scroll_offset =
3941                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
3942                    }
3943                } else if self.current_service == Service::CloudWatchLogGroups {
3944                    if self.view_mode == ViewMode::List {
3945                        self.log_groups_state.log_groups.prev_item();
3946                    } else if self.view_mode == ViewMode::Detail
3947                        && self.log_groups_state.selected_stream > 0
3948                    {
3949                        self.log_groups_state.selected_stream =
3950                            self.log_groups_state.selected_stream.saturating_sub(1);
3951                        self.log_groups_state.expanded_stream = None;
3952                    }
3953                } else if self.current_service == Service::CloudWatchAlarms {
3954                    self.alarms_state.table.prev_item();
3955                } else if self.current_service == Service::EcrRepositories {
3956                    if self.ecr_state.current_repository.is_some() {
3957                        self.ecr_state.images.prev_item();
3958                    } else {
3959                        self.ecr_state.repositories.prev_item();
3960                    }
3961                } else if self.current_service == Service::SqsQueues {
3962                    if self.sqs_state.current_queue.is_some()
3963                        && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
3964                    {
3965                        self.sqs_state.triggers.prev_item();
3966                    } else if self.sqs_state.current_queue.is_some()
3967                        && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
3968                    {
3969                        self.sqs_state.pipes.prev_item();
3970                    } else if self.sqs_state.current_queue.is_some()
3971                        && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
3972                    {
3973                        self.sqs_state.tags.prev_item();
3974                    } else if self.sqs_state.current_queue.is_some()
3975                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3976                    {
3977                        self.sqs_state.subscriptions.prev_item();
3978                    } else {
3979                        self.sqs_state.queues.prev_item();
3980                    }
3981                } else if self.current_service == Service::LambdaFunctions {
3982                    if self.lambda_state.current_function.is_some()
3983                        && self.lambda_state.detail_tab == LambdaDetailTab::Code
3984                    {
3985                        // Layer table navigation in Code tab
3986                        self.lambda_state.layer_selected =
3987                            self.lambda_state.layer_selected.saturating_sub(1);
3988                    } else if self.lambda_state.current_function.is_some()
3989                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3990                    {
3991                        self.lambda_state.version_table.prev_item();
3992                    } else if self.lambda_state.current_function.is_some()
3993                        && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3994                            || (self.lambda_state.current_version.is_some()
3995                                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3996                    {
3997                        self.lambda_state.alias_table.prev_item();
3998                    } else if self.lambda_state.current_function.is_none() {
3999                        self.lambda_state.table.prev_item();
4000                    }
4001                } else if self.current_service == Service::LambdaApplications {
4002                    if self.lambda_application_state.current_application.is_some()
4003                        && self.lambda_application_state.detail_tab
4004                            == LambdaApplicationDetailTab::Overview
4005                    {
4006                        self.lambda_application_state.resources.selected = self
4007                            .lambda_application_state
4008                            .resources
4009                            .selected
4010                            .saturating_sub(1);
4011                    } else if self.lambda_application_state.current_application.is_some()
4012                        && self.lambda_application_state.detail_tab
4013                            == LambdaApplicationDetailTab::Deployments
4014                    {
4015                        self.lambda_application_state.deployments.selected = self
4016                            .lambda_application_state
4017                            .deployments
4018                            .selected
4019                            .saturating_sub(1);
4020                    } else {
4021                        self.lambda_application_state.table.selected = self
4022                            .lambda_application_state
4023                            .table
4024                            .selected
4025                            .saturating_sub(1);
4026                        self.lambda_application_state.table.snap_to_page();
4027                    }
4028                } else if self.current_service == Service::CloudFormationStacks {
4029                    self.cfn_state.table.prev_item();
4030                } else if self.current_service == Service::IamUsers {
4031                    self.iam_state.users.prev_item();
4032                } else if self.current_service == Service::IamRoles {
4033                    if self.iam_state.current_role.is_some() {
4034                        if self.iam_state.role_tab == RoleTab::TrustRelationships {
4035                            self.iam_state.trust_policy_scroll =
4036                                self.iam_state.trust_policy_scroll.saturating_sub(1);
4037                        } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
4038                            self.iam_state.revoke_sessions_scroll =
4039                                self.iam_state.revoke_sessions_scroll.saturating_sub(1);
4040                        } else if self.iam_state.role_tab == RoleTab::Tags {
4041                            self.iam_state.tags.prev_item();
4042                        } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4043                            self.iam_state.last_accessed_services.prev_item();
4044                        } else {
4045                            self.iam_state.policies.prev_item();
4046                        }
4047                    } else {
4048                        self.iam_state.roles.prev_item();
4049                    }
4050                } else if self.current_service == Service::IamUserGroups {
4051                    if self.iam_state.current_group.is_some() {
4052                        if self.iam_state.group_tab == GroupTab::Users {
4053                            self.iam_state.group_users.prev_item();
4054                        } else if self.iam_state.group_tab == GroupTab::Permissions {
4055                            self.iam_state.policies.prev_item();
4056                        } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
4057                            self.iam_state.last_accessed_services.prev_item();
4058                        }
4059                    } else {
4060                        self.iam_state.groups.prev_item();
4061                    }
4062                }
4063            }
4064            _ => {}
4065        }
4066    }
4067
4068    fn page_down(&mut self) {
4069        if self.mode == Mode::FilterInput && self.current_service == Service::CloudFormationStacks {
4070            use crate::ui::cfn::filtered_cloudformation_stacks;
4071            let page_size = self.cfn_state.table.page_size.value();
4072            let filtered_count = filtered_cloudformation_stacks(self).len();
4073            self.cfn_state.input_focus.handle_page_down(
4074                &mut self.cfn_state.table.selected,
4075                &mut self.cfn_state.table.scroll_offset,
4076                page_size,
4077                filtered_count,
4078            );
4079        } else if self.mode == Mode::FilterInput
4080            && self.current_service == Service::IamRoles
4081            && self.iam_state.current_role.is_none()
4082        {
4083            let page_size = self.iam_state.roles.page_size.value();
4084            let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
4085            self.iam_state.role_input_focus.handle_page_down(
4086                &mut self.iam_state.roles.selected,
4087                &mut self.iam_state.roles.scroll_offset,
4088                page_size,
4089                filtered_count,
4090            );
4091        } else if self.mode == Mode::FilterInput
4092            && self.current_service == Service::CloudWatchAlarms
4093        {
4094            let page_size = self.alarms_state.table.page_size.value();
4095            let filtered_count = self.alarms_state.table.items.len();
4096            self.alarms_state.input_focus.handle_page_down(
4097                &mut self.alarms_state.table.selected,
4098                &mut self.alarms_state.table.scroll_offset,
4099                page_size,
4100                filtered_count,
4101            );
4102        } else if self.mode == Mode::FilterInput
4103            && self.current_service == Service::CloudWatchLogGroups
4104        {
4105            if self.view_mode == ViewMode::List {
4106                // Log groups list pagination
4107                let filtered = self.filtered_log_groups();
4108                let page_size = self.log_groups_state.log_groups.page_size.value();
4109                let filtered_count = filtered.len();
4110                self.log_groups_state.input_focus.handle_page_down(
4111                    &mut self.log_groups_state.log_groups.selected,
4112                    &mut self.log_groups_state.log_groups.scroll_offset,
4113                    page_size,
4114                    filtered_count,
4115                );
4116            } else {
4117                // Log streams pagination
4118                let filtered = self.filtered_log_streams();
4119                let page_size = 20;
4120                let filtered_count = filtered.len();
4121                self.log_groups_state.input_focus.handle_page_down(
4122                    &mut self.log_groups_state.selected_stream,
4123                    &mut self.log_groups_state.stream_page,
4124                    page_size,
4125                    filtered_count,
4126                );
4127                self.log_groups_state.expanded_stream = None;
4128            }
4129        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
4130        {
4131            if self.lambda_state.current_function.is_some()
4132                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4133                && self.lambda_state.version_input_focus == InputFocus::Pagination
4134            {
4135                let page_size = self.lambda_state.version_table.page_size.value();
4136                let filtered_count: usize = self
4137                    .lambda_state
4138                    .version_table
4139                    .items
4140                    .iter()
4141                    .filter(|v| {
4142                        self.lambda_state.version_table.filter.is_empty()
4143                            || v.version
4144                                .to_lowercase()
4145                                .contains(&self.lambda_state.version_table.filter.to_lowercase())
4146                            || v.aliases
4147                                .to_lowercase()
4148                                .contains(&self.lambda_state.version_table.filter.to_lowercase())
4149                            || v.description
4150                                .to_lowercase()
4151                                .contains(&self.lambda_state.version_table.filter.to_lowercase())
4152                    })
4153                    .count();
4154                let target = self.lambda_state.version_table.selected + page_size;
4155                self.lambda_state.version_table.selected =
4156                    target.min(filtered_count.saturating_sub(1));
4157            } else if self.lambda_state.current_function.is_some()
4158                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4159                    || (self.lambda_state.current_version.is_some()
4160                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4161                && self.lambda_state.alias_input_focus == InputFocus::Pagination
4162            {
4163                let page_size = self.lambda_state.alias_table.page_size.value();
4164                let version_filter = self.lambda_state.current_version.clone();
4165                let filtered_count = self
4166                    .lambda_state
4167                    .alias_table
4168                    .items
4169                    .iter()
4170                    .filter(|a| {
4171                        (version_filter.is_none()
4172                            || a.versions.contains(version_filter.as_ref().unwrap()))
4173                            && (self.lambda_state.alias_table.filter.is_empty()
4174                                || a.name
4175                                    .to_lowercase()
4176                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
4177                                || a.versions
4178                                    .to_lowercase()
4179                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
4180                                || a.description
4181                                    .to_lowercase()
4182                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase()))
4183                    })
4184                    .count();
4185                let target = self.lambda_state.alias_table.selected + page_size;
4186                self.lambda_state.alias_table.selected =
4187                    target.min(filtered_count.saturating_sub(1));
4188            } else if self.lambda_state.current_function.is_none() {
4189                let page_size = self.lambda_state.table.page_size.value();
4190                let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
4191                self.lambda_state.input_focus.handle_page_down(
4192                    &mut self.lambda_state.table.selected,
4193                    &mut self.lambda_state.table.scroll_offset,
4194                    page_size,
4195                    filtered_count,
4196                );
4197            }
4198        } else if self.mode == Mode::FilterInput
4199            && self.current_service == Service::EcrRepositories
4200            && self.ecr_state.current_repository.is_none()
4201            && self.ecr_state.input_focus == InputFocus::Filter
4202        {
4203            // When input is focused, allow table scrolling
4204            let filtered = self.filtered_ecr_repositories();
4205            self.ecr_state.repositories.page_down(filtered.len());
4206        } else if self.mode == Mode::FilterInput
4207            && self.current_service == Service::EcrRepositories
4208            && self.ecr_state.current_repository.is_none()
4209        {
4210            let page_size = self.ecr_state.repositories.page_size.value();
4211            let filtered_count = self.filtered_ecr_repositories().len();
4212            self.ecr_state.input_focus.handle_page_down(
4213                &mut self.ecr_state.repositories.selected,
4214                &mut self.ecr_state.repositories.scroll_offset,
4215                page_size,
4216                filtered_count,
4217            );
4218        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
4219            let page_size = self.iam_state.policies.page_size.value();
4220            let filtered_count = crate::ui::iam::filtered_iam_policies(self).len();
4221            self.iam_state.policy_input_focus.handle_page_down(
4222                &mut self.iam_state.policies.selected,
4223                &mut self.iam_state.policies.scroll_offset,
4224                page_size,
4225                filtered_count,
4226            );
4227        } else if self.view_mode == ViewMode::PolicyView {
4228            let lines = self.iam_state.policy_document.lines().count();
4229            let max_scroll = lines.saturating_sub(1);
4230            self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
4231        } else if self.current_service == Service::SqsQueues
4232            && self.sqs_state.current_queue.is_some()
4233        {
4234            if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
4235                self.sqs_state.monitoring_scroll = (self.sqs_state.monitoring_scroll + 1).min(8);
4236            } else {
4237                let lines = self.sqs_state.policy_document.lines().count();
4238                let max_scroll = lines.saturating_sub(1);
4239                self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
4240            }
4241        } else if self.current_service == Service::IamRoles
4242            && self.iam_state.current_role.is_some()
4243            && self.iam_state.role_tab == RoleTab::TrustRelationships
4244        {
4245            let lines = self.iam_state.trust_policy_document.lines().count();
4246            let max_scroll = lines.saturating_sub(1);
4247            self.iam_state.trust_policy_scroll =
4248                (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
4249        } else if self.current_service == Service::IamRoles
4250            && self.iam_state.current_role.is_some()
4251            && self.iam_state.role_tab == RoleTab::RevokeSessions
4252        {
4253            self.iam_state.revoke_sessions_scroll =
4254                (self.iam_state.revoke_sessions_scroll + 10).min(19);
4255        } else if self.mode == Mode::Normal {
4256            if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
4257            {
4258                let total_rows = self.calculate_total_bucket_rows();
4259                self.s3_state.selected_row = self
4260                    .s3_state
4261                    .selected_row
4262                    .saturating_add(10)
4263                    .min(total_rows.saturating_sub(1));
4264
4265                // Adjust scroll offset if selection goes below viewport
4266                let visible_rows = self.s3_state.bucket_visible_rows.get();
4267                if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
4268                    self.s3_state.bucket_scroll_offset =
4269                        self.s3_state.selected_row - visible_rows + 1;
4270                }
4271            } else if self.current_service == Service::S3Buckets
4272                && self.s3_state.current_bucket.is_some()
4273            {
4274                let total_rows = self.calculate_total_object_rows();
4275                self.s3_state.selected_object = self
4276                    .s3_state
4277                    .selected_object
4278                    .saturating_add(10)
4279                    .min(total_rows.saturating_sub(1));
4280
4281                // Adjust scroll offset if selection goes below viewport
4282                let visible_rows = self.s3_state.object_visible_rows.get();
4283                if self.s3_state.selected_object
4284                    >= self.s3_state.object_scroll_offset + visible_rows
4285                {
4286                    self.s3_state.object_scroll_offset =
4287                        self.s3_state.selected_object - visible_rows + 1;
4288                }
4289            } else if self.current_service == Service::CloudWatchLogGroups
4290                && self.view_mode == ViewMode::List
4291            {
4292                let filtered = self.filtered_log_groups();
4293                self.log_groups_state.log_groups.page_down(filtered.len());
4294            } else if self.current_service == Service::CloudWatchLogGroups
4295                && self.view_mode == ViewMode::Detail
4296            {
4297                let len = self.filtered_log_streams().len();
4298                nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
4299            } else if self.view_mode == ViewMode::Events {
4300                let max = self.log_groups_state.log_events.len();
4301                nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
4302            } else if self.view_mode == ViewMode::InsightsResults {
4303                let max = self.insights_state.insights.query_results.len();
4304                nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
4305            } else if self.current_service == Service::CloudWatchAlarms {
4306                let filtered = match self.alarms_state.alarm_tab {
4307                    AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4308                    AlarmTab::InAlarm => self
4309                        .alarms_state
4310                        .table
4311                        .items
4312                        .iter()
4313                        .filter(|a| a.state.to_uppercase() == "ALARM")
4314                        .count(),
4315                };
4316                if filtered > 0 {
4317                    self.alarms_state.table.page_down(filtered);
4318                }
4319            } else if self.current_service == Service::EcrRepositories {
4320                if self.ecr_state.current_repository.is_some() {
4321                    let filtered = self.filtered_ecr_images();
4322                    self.ecr_state.images.page_down(filtered.len());
4323                } else {
4324                    let filtered = self.filtered_ecr_repositories();
4325                    self.ecr_state.repositories.page_down(filtered.len());
4326                }
4327            } else if self.current_service == Service::SqsQueues {
4328                let filtered = crate::ui::sqs::filtered_queues(
4329                    &self.sqs_state.queues.items,
4330                    &self.sqs_state.queues.filter,
4331                );
4332                self.sqs_state.queues.page_down(filtered.len());
4333            } else if self.current_service == Service::LambdaFunctions {
4334                let len = crate::ui::lambda::filtered_lambda_functions(self).len();
4335                self.lambda_state.table.page_down(len);
4336            } else if self.current_service == Service::LambdaApplications {
4337                let len = crate::ui::lambda::filtered_lambda_applications(self).len();
4338                self.lambda_application_state.table.page_down(len);
4339            } else if self.current_service == Service::CloudFormationStacks {
4340                let filtered = self.filtered_cloudformation_stacks();
4341                self.cfn_state.table.page_down(filtered.len());
4342            } else if self.current_service == Service::IamUsers {
4343                let len = crate::ui::iam::filtered_iam_users(self).len();
4344                nav_page_down(&mut self.iam_state.users.selected, len, 10);
4345            } else if self.current_service == Service::IamRoles {
4346                if self.iam_state.current_role.is_some() {
4347                    let filtered = crate::ui::iam::filtered_iam_policies(self);
4348                    if !filtered.is_empty() {
4349                        self.iam_state.policies.page_down(filtered.len());
4350                    }
4351                } else {
4352                    let filtered = crate::ui::iam::filtered_iam_roles(self);
4353                    self.iam_state.roles.page_down(filtered.len());
4354                }
4355            } else if self.current_service == Service::IamUserGroups {
4356                if self.iam_state.current_group.is_some() {
4357                    if self.iam_state.group_tab == GroupTab::Users {
4358                        let filtered: Vec<_> = self
4359                            .iam_state
4360                            .group_users
4361                            .items
4362                            .iter()
4363                            .filter(|u| {
4364                                if self.iam_state.group_users.filter.is_empty() {
4365                                    true
4366                                } else {
4367                                    u.user_name
4368                                        .to_lowercase()
4369                                        .contains(&self.iam_state.group_users.filter.to_lowercase())
4370                                }
4371                            })
4372                            .collect();
4373                        if !filtered.is_empty() {
4374                            self.iam_state.group_users.page_down(filtered.len());
4375                        }
4376                    } else if self.iam_state.group_tab == GroupTab::Permissions {
4377                        let filtered = crate::ui::iam::filtered_iam_policies(self);
4378                        if !filtered.is_empty() {
4379                            self.iam_state.policies.page_down(filtered.len());
4380                        }
4381                    } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
4382                        let filtered = crate::ui::iam::filtered_last_accessed(self);
4383                        if !filtered.is_empty() {
4384                            self.iam_state
4385                                .last_accessed_services
4386                                .page_down(filtered.len());
4387                        }
4388                    }
4389                } else {
4390                    let filtered: Vec<_> = self
4391                        .iam_state
4392                        .groups
4393                        .items
4394                        .iter()
4395                        .filter(|g| {
4396                            if self.iam_state.groups.filter.is_empty() {
4397                                true
4398                            } else {
4399                                g.group_name
4400                                    .to_lowercase()
4401                                    .contains(&self.iam_state.groups.filter.to_lowercase())
4402                            }
4403                        })
4404                        .collect();
4405                    if !filtered.is_empty() {
4406                        self.iam_state.groups.page_down(filtered.len());
4407                    }
4408                }
4409            }
4410        }
4411    }
4412
4413    fn page_up(&mut self) {
4414        if self.mode == Mode::FilterInput && self.current_service == Service::CloudFormationStacks {
4415            let page_size = self.cfn_state.table.page_size.value();
4416            self.cfn_state.input_focus.handle_page_up(
4417                &mut self.cfn_state.table.selected,
4418                &mut self.cfn_state.table.scroll_offset,
4419                page_size,
4420            );
4421        } else if self.mode == Mode::FilterInput
4422            && self.current_service == Service::IamRoles
4423            && self.iam_state.current_role.is_none()
4424        {
4425            let page_size = self.iam_state.roles.page_size.value();
4426            self.iam_state.role_input_focus.handle_page_up(
4427                &mut self.iam_state.roles.selected,
4428                &mut self.iam_state.roles.scroll_offset,
4429                page_size,
4430            );
4431        } else if self.mode == Mode::FilterInput
4432            && self.current_service == Service::CloudWatchAlarms
4433        {
4434            let page_size = self.alarms_state.table.page_size.value();
4435            self.alarms_state.input_focus.handle_page_up(
4436                &mut self.alarms_state.table.selected,
4437                &mut self.alarms_state.table.scroll_offset,
4438                page_size,
4439            );
4440        } else if self.mode == Mode::FilterInput
4441            && self.current_service == Service::CloudWatchLogGroups
4442        {
4443            if self.view_mode == ViewMode::List {
4444                // Log groups list pagination
4445                let page_size = self.log_groups_state.log_groups.page_size.value();
4446                self.log_groups_state.input_focus.handle_page_up(
4447                    &mut self.log_groups_state.log_groups.selected,
4448                    &mut self.log_groups_state.log_groups.scroll_offset,
4449                    page_size,
4450                );
4451            } else {
4452                // Log streams pagination
4453                let page_size = 20;
4454                self.log_groups_state.input_focus.handle_page_up(
4455                    &mut self.log_groups_state.selected_stream,
4456                    &mut self.log_groups_state.stream_page,
4457                    page_size,
4458                );
4459                self.log_groups_state.expanded_stream = None;
4460            }
4461        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
4462        {
4463            if self.lambda_state.current_function.is_some()
4464                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4465                && self.lambda_state.version_input_focus == InputFocus::Pagination
4466            {
4467                let page_size = self.lambda_state.version_table.page_size.value();
4468                self.lambda_state.version_table.selected = self
4469                    .lambda_state
4470                    .version_table
4471                    .selected
4472                    .saturating_sub(page_size);
4473            } else if self.lambda_state.current_function.is_some()
4474                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4475                    || (self.lambda_state.current_version.is_some()
4476                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4477                && self.lambda_state.alias_input_focus == InputFocus::Pagination
4478            {
4479                let page_size = self.lambda_state.alias_table.page_size.value();
4480                self.lambda_state.alias_table.selected = self
4481                    .lambda_state
4482                    .alias_table
4483                    .selected
4484                    .saturating_sub(page_size);
4485            } else if self.lambda_state.current_function.is_none() {
4486                let page_size = self.lambda_state.table.page_size.value();
4487                self.lambda_state.input_focus.handle_page_up(
4488                    &mut self.lambda_state.table.selected,
4489                    &mut self.lambda_state.table.scroll_offset,
4490                    page_size,
4491                );
4492            }
4493        } else if self.mode == Mode::FilterInput
4494            && self.current_service == Service::EcrRepositories
4495            && self.ecr_state.current_repository.is_none()
4496            && self.ecr_state.input_focus == InputFocus::Filter
4497        {
4498            // When input is focused, allow table scrolling
4499            self.ecr_state.repositories.page_up();
4500        } else if self.mode == Mode::FilterInput
4501            && self.current_service == Service::EcrRepositories
4502            && self.ecr_state.current_repository.is_none()
4503        {
4504            let page_size = self.ecr_state.repositories.page_size.value();
4505            self.ecr_state.input_focus.handle_page_up(
4506                &mut self.ecr_state.repositories.selected,
4507                &mut self.ecr_state.repositories.scroll_offset,
4508                page_size,
4509            );
4510        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
4511            let page_size = self.iam_state.policies.page_size.value();
4512            self.iam_state.policy_input_focus.handle_page_up(
4513                &mut self.iam_state.policies.selected,
4514                &mut self.iam_state.policies.scroll_offset,
4515                page_size,
4516            );
4517        } else if self.view_mode == ViewMode::PolicyView {
4518            self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
4519        } else if self.current_service == Service::SqsQueues
4520            && self.sqs_state.current_queue.is_some()
4521        {
4522            if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
4523                self.sqs_state.monitoring_scroll =
4524                    self.sqs_state.monitoring_scroll.saturating_sub(1);
4525            } else {
4526                self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
4527            }
4528        } else if self.current_service == Service::IamRoles
4529            && self.iam_state.current_role.is_some()
4530            && self.iam_state.role_tab == RoleTab::TrustRelationships
4531        {
4532            self.iam_state.trust_policy_scroll =
4533                self.iam_state.trust_policy_scroll.saturating_sub(10);
4534        } else if self.current_service == Service::IamRoles
4535            && self.iam_state.current_role.is_some()
4536            && self.iam_state.role_tab == RoleTab::RevokeSessions
4537        {
4538            self.iam_state.revoke_sessions_scroll =
4539                self.iam_state.revoke_sessions_scroll.saturating_sub(10);
4540        } else if self.mode == Mode::Normal {
4541            if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
4542            {
4543                self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
4544
4545                // Adjust scroll offset if selection goes above viewport
4546                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4547                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4548                }
4549            } else if self.current_service == Service::S3Buckets
4550                && self.s3_state.current_bucket.is_some()
4551            {
4552                self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
4553
4554                // Adjust scroll offset if selection goes above viewport
4555                if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
4556                    self.s3_state.object_scroll_offset = self.s3_state.selected_object;
4557                }
4558            } else if self.current_service == Service::CloudWatchLogGroups
4559                && self.view_mode == ViewMode::List
4560            {
4561                self.log_groups_state.log_groups.page_up();
4562            } else if self.current_service == Service::CloudWatchLogGroups
4563                && self.view_mode == ViewMode::Detail
4564            {
4565                self.log_groups_state.selected_stream =
4566                    self.log_groups_state.selected_stream.saturating_sub(10);
4567            } else if self.view_mode == ViewMode::Events {
4568                if self.log_groups_state.event_scroll_offset < 10
4569                    && self.log_groups_state.has_older_events
4570                {
4571                    self.log_groups_state.loading = true;
4572                }
4573                self.log_groups_state.event_scroll_offset =
4574                    self.log_groups_state.event_scroll_offset.saturating_sub(10);
4575            } else if self.view_mode == ViewMode::InsightsResults {
4576                self.insights_state.insights.results_selected = self
4577                    .insights_state
4578                    .insights
4579                    .results_selected
4580                    .saturating_sub(10);
4581            } else if self.current_service == Service::CloudWatchAlarms {
4582                self.alarms_state.table.page_up();
4583            } else if self.current_service == Service::EcrRepositories {
4584                if self.ecr_state.current_repository.is_some() {
4585                    self.ecr_state.images.page_up();
4586                } else {
4587                    self.ecr_state.repositories.page_up();
4588                }
4589            } else if self.current_service == Service::SqsQueues {
4590                self.sqs_state.queues.page_up();
4591            } else if self.current_service == Service::LambdaFunctions {
4592                self.lambda_state.table.page_up();
4593            } else if self.current_service == Service::LambdaApplications {
4594                self.lambda_application_state.table.page_up();
4595            } else if self.current_service == Service::CloudFormationStacks {
4596                self.cfn_state.table.page_up();
4597            } else if self.current_service == Service::IamUsers {
4598                self.iam_state.users.page_up();
4599            } else if self.current_service == Service::IamRoles {
4600                if self.iam_state.current_role.is_some() {
4601                    self.iam_state.policies.page_up();
4602                } else {
4603                    self.iam_state.roles.page_up();
4604                }
4605            }
4606        }
4607    }
4608
4609    fn next_pane(&mut self) {
4610        if self.current_service == Service::S3Buckets {
4611            if self.s3_state.current_bucket.is_some() {
4612                // In objects view - expand prefix and trigger preview load
4613                // Map visual index to actual object (including nested items)
4614                let mut visual_idx = 0;
4615                let mut found_obj: Option<S3Object> = None;
4616
4617                // Helper to recursively check nested items
4618                fn check_nested(
4619                    obj: &S3Object,
4620                    visual_idx: &mut usize,
4621                    target_idx: usize,
4622                    expanded_prefixes: &std::collections::HashSet<String>,
4623                    prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
4624                    found_obj: &mut Option<S3Object>,
4625                ) {
4626                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4627                        if let Some(preview) = prefix_preview.get(&obj.key) {
4628                            for nested_obj in preview {
4629                                if *visual_idx == target_idx {
4630                                    *found_obj = Some(nested_obj.clone());
4631                                    return;
4632                                }
4633                                *visual_idx += 1;
4634
4635                                // Recursively check deeper levels
4636                                check_nested(
4637                                    nested_obj,
4638                                    visual_idx,
4639                                    target_idx,
4640                                    expanded_prefixes,
4641                                    prefix_preview,
4642                                    found_obj,
4643                                );
4644                                if found_obj.is_some() {
4645                                    return;
4646                                }
4647                            }
4648                        } else {
4649                            // Loading row
4650                            *visual_idx += 1;
4651                        }
4652                    }
4653                }
4654
4655                for obj in &self.s3_state.objects {
4656                    if visual_idx == self.s3_state.selected_object {
4657                        found_obj = Some(obj.clone());
4658                        break;
4659                    }
4660                    visual_idx += 1;
4661
4662                    // Check nested items recursively
4663                    check_nested(
4664                        obj,
4665                        &mut visual_idx,
4666                        self.s3_state.selected_object,
4667                        &self.s3_state.expanded_prefixes,
4668                        &self.s3_state.prefix_preview,
4669                        &mut found_obj,
4670                    );
4671                    if found_obj.is_some() {
4672                        break;
4673                    }
4674                }
4675
4676                if let Some(obj) = found_obj {
4677                    if obj.is_prefix {
4678                        if !self.s3_state.expanded_prefixes.contains(&obj.key) {
4679                            self.s3_state.expanded_prefixes.insert(obj.key.clone());
4680                            // Trigger preview load if not already cached
4681                            if !self.s3_state.prefix_preview.contains_key(&obj.key) {
4682                                self.s3_state.buckets.loading = true;
4683                            }
4684                        }
4685                        // Move to first child if expanded and has children
4686                        if self.s3_state.expanded_prefixes.contains(&obj.key) {
4687                            if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
4688                                if !preview.is_empty() {
4689                                    self.s3_state.selected_object += 1;
4690                                }
4691                            }
4692                        }
4693                    }
4694                }
4695            } else {
4696                // In bucket list - find which bucket/prefix the selected row corresponds to
4697                let mut row_idx = 0;
4698                let mut found = false;
4699                for bucket in &self.s3_state.buckets.items {
4700                    if row_idx == self.s3_state.selected_row {
4701                        // Selected row is a bucket - expand and move to first child
4702                        if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
4703                            self.s3_state.expanded_prefixes.insert(bucket.name.clone());
4704                            if !self.s3_state.bucket_preview.contains_key(&bucket.name)
4705                                && !self.s3_state.bucket_errors.contains_key(&bucket.name)
4706                            {
4707                                self.s3_state.buckets.loading = true;
4708                            }
4709                        }
4710                        // Move to first child if expanded and has children
4711                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4712                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4713                                if !preview.is_empty() {
4714                                    self.s3_state.selected_row = row_idx + 1;
4715                                }
4716                            }
4717                        }
4718                        break;
4719                    }
4720                    row_idx += 1;
4721
4722                    // Skip error rows - they're not selectable
4723                    if self.s3_state.bucket_errors.contains_key(&bucket.name)
4724                        && self.s3_state.expanded_prefixes.contains(&bucket.name)
4725                    {
4726                        continue;
4727                    }
4728
4729                    if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4730                        if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4731                            // Recursive function to check nested items at any depth
4732                            #[allow(clippy::too_many_arguments)]
4733                            fn check_nested_expansion(
4734                                objects: &[crate::s3::Object],
4735                                row_idx: &mut usize,
4736                                target_row: usize,
4737                                expanded_prefixes: &mut std::collections::HashSet<String>,
4738                                prefix_preview: &std::collections::HashMap<
4739                                    String,
4740                                    Vec<crate::s3::Object>,
4741                                >,
4742                                found: &mut bool,
4743                                loading: &mut bool,
4744                                selected_row: &mut usize,
4745                            ) {
4746                                for obj in objects {
4747                                    if *row_idx == target_row {
4748                                        // Selected this item - expand and move to first child
4749                                        if obj.is_prefix {
4750                                            if !expanded_prefixes.contains(&obj.key) {
4751                                                expanded_prefixes.insert(obj.key.clone());
4752                                                if !prefix_preview.contains_key(&obj.key) {
4753                                                    *loading = true;
4754                                                }
4755                                            }
4756                                            // Move to first child if expanded and has children
4757                                            if expanded_prefixes.contains(&obj.key) {
4758                                                if let Some(preview) = prefix_preview.get(&obj.key)
4759                                                {
4760                                                    if !preview.is_empty() {
4761                                                        *selected_row = *row_idx + 1;
4762                                                    }
4763                                                }
4764                                            }
4765                                        }
4766                                        *found = true;
4767                                        return;
4768                                    }
4769                                    *row_idx += 1;
4770
4771                                    // Recursively check nested items if expanded
4772                                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4773                                        if let Some(nested) = prefix_preview.get(&obj.key) {
4774                                            check_nested_expansion(
4775                                                nested,
4776                                                row_idx,
4777                                                target_row,
4778                                                expanded_prefixes,
4779                                                prefix_preview,
4780                                                found,
4781                                                loading,
4782                                                selected_row,
4783                                            );
4784                                            if *found {
4785                                                return;
4786                                            }
4787                                        } else {
4788                                            *row_idx += 1; // Loading row
4789                                        }
4790                                    }
4791                                }
4792                            }
4793
4794                            check_nested_expansion(
4795                                preview,
4796                                &mut row_idx,
4797                                self.s3_state.selected_row,
4798                                &mut self.s3_state.expanded_prefixes,
4799                                &self.s3_state.prefix_preview,
4800                                &mut found,
4801                                &mut self.s3_state.buckets.loading,
4802                                &mut self.s3_state.selected_row,
4803                            );
4804                            if found || row_idx > self.s3_state.selected_row {
4805                                break;
4806                            }
4807                        } else {
4808                            row_idx += 1;
4809                            if row_idx > self.s3_state.selected_row {
4810                                break;
4811                            }
4812                        }
4813                    }
4814                    if found {
4815                        break;
4816                    }
4817                }
4818            }
4819        } else if self.view_mode == ViewMode::InsightsResults {
4820            // Right arrow scrolls horizontally by 1 column
4821            let max_cols = self
4822                .insights_state
4823                .insights
4824                .query_results
4825                .first()
4826                .map(|r| r.len())
4827                .unwrap_or(0);
4828            if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
4829                self.insights_state.insights.results_horizontal_scroll += 1;
4830            }
4831        } else if self.current_service == Service::CloudWatchLogGroups
4832            && self.view_mode == ViewMode::List
4833        {
4834            // Expand selected log group
4835            if self.log_groups_state.log_groups.expanded_item
4836                != Some(self.log_groups_state.log_groups.selected)
4837            {
4838                self.log_groups_state.log_groups.expanded_item =
4839                    Some(self.log_groups_state.log_groups.selected);
4840            }
4841        } else if self.current_service == Service::CloudWatchLogGroups
4842            && self.view_mode == ViewMode::Detail
4843        {
4844            // Expand selected log stream
4845            if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
4846            {
4847                self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
4848            }
4849        } else if self.view_mode == ViewMode::Events {
4850            // Only scroll if there are hidden columns
4851            // Expand selected event
4852            if self.log_groups_state.expanded_event
4853                != Some(self.log_groups_state.event_scroll_offset)
4854            {
4855                self.log_groups_state.expanded_event =
4856                    Some(self.log_groups_state.event_scroll_offset);
4857            }
4858        } else if self.current_service == Service::CloudWatchAlarms {
4859            // Expand selected alarm
4860            if !self.alarms_state.table.is_expanded() {
4861                self.alarms_state.table.toggle_expand();
4862            }
4863        } else if self.current_service == Service::EcrRepositories {
4864            if self.ecr_state.current_repository.is_some() {
4865                // In images view - expand selected image
4866                self.ecr_state.images.toggle_expand();
4867            } else {
4868                // In repositories view - expand selected repository
4869                self.ecr_state.repositories.toggle_expand();
4870            }
4871        } else if self.current_service == Service::SqsQueues {
4872            if self.sqs_state.current_queue.is_some()
4873                && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
4874            {
4875                self.sqs_state.triggers.toggle_expand();
4876            } else if self.sqs_state.current_queue.is_some()
4877                && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
4878            {
4879                self.sqs_state.pipes.toggle_expand();
4880            } else if self.sqs_state.current_queue.is_some()
4881                && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
4882            {
4883                self.sqs_state.tags.toggle_expand();
4884            } else if self.sqs_state.current_queue.is_some()
4885                && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
4886            {
4887                self.sqs_state.subscriptions.toggle_expand();
4888            } else {
4889                self.sqs_state.queues.expand();
4890            }
4891        } else if self.current_service == Service::LambdaFunctions {
4892            if self.lambda_state.current_function.is_some()
4893                && self.lambda_state.detail_tab == LambdaDetailTab::Code
4894            {
4895                // Expand selected layer
4896                if self.lambda_state.layer_expanded != Some(self.lambda_state.layer_selected) {
4897                    self.lambda_state.layer_expanded = Some(self.lambda_state.layer_selected);
4898                } else {
4899                    self.lambda_state.layer_expanded = None;
4900                }
4901            } else if self.lambda_state.current_function.is_some()
4902                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4903            {
4904                // Expand selected version
4905                self.lambda_state.version_table.toggle_expand();
4906            } else if self.lambda_state.current_function.is_some()
4907                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4908                    || (self.lambda_state.current_version.is_some()
4909                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4910            {
4911                // Expand selected alias
4912                self.lambda_state.alias_table.toggle_expand();
4913            } else if self.lambda_state.current_function.is_none() {
4914                // Expand selected function
4915                self.lambda_state.table.toggle_expand();
4916            }
4917        } else if self.current_service == Service::LambdaApplications {
4918            if self.lambda_application_state.current_application.is_some() {
4919                // In detail view - expand resource or deployment
4920                if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
4921                {
4922                    self.lambda_application_state.resources.toggle_expand();
4923                } else {
4924                    self.lambda_application_state.deployments.toggle_expand();
4925                }
4926            } else {
4927                // Expand selected application in list
4928                if self.lambda_application_state.table.expanded_item
4929                    != Some(self.lambda_application_state.table.selected)
4930                {
4931                    self.lambda_application_state.table.expanded_item =
4932                        Some(self.lambda_application_state.table.selected);
4933                }
4934            }
4935        } else if self.current_service == Service::CloudFormationStacks
4936            && self.cfn_state.current_stack.is_none()
4937        {
4938            self.cfn_state.table.toggle_expand();
4939        } else if self.current_service == Service::IamUsers {
4940            if self.iam_state.current_user.is_some() {
4941                if self.iam_state.user_tab == UserTab::Tags {
4942                    if self.iam_state.user_tags.expanded_item
4943                        != Some(self.iam_state.user_tags.selected)
4944                    {
4945                        self.iam_state.user_tags.expanded_item =
4946                            Some(self.iam_state.user_tags.selected);
4947                    }
4948                } else if self.iam_state.policies.expanded_item
4949                    != Some(self.iam_state.policies.selected)
4950                {
4951                    self.iam_state.policies.toggle_expand();
4952                }
4953            } else if !self.iam_state.users.is_expanded() {
4954                self.iam_state.users.toggle_expand();
4955            }
4956        } else if self.current_service == Service::IamRoles {
4957            if self.iam_state.current_role.is_some() {
4958                // Handle expansion based on current tab
4959                if self.iam_state.role_tab == RoleTab::Tags {
4960                    if !self.iam_state.tags.is_expanded() {
4961                        self.iam_state.tags.expand();
4962                    }
4963                } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4964                    if !self.iam_state.last_accessed_services.is_expanded() {
4965                        self.iam_state.last_accessed_services.expand();
4966                    }
4967                } else if !self.iam_state.policies.is_expanded() {
4968                    self.iam_state.policies.expand();
4969                }
4970            } else if !self.iam_state.roles.is_expanded() {
4971                self.iam_state.roles.expand();
4972            }
4973        } else if self.current_service == Service::IamUserGroups {
4974            if self.iam_state.current_group.is_some() {
4975                if self.iam_state.group_tab == GroupTab::Users {
4976                    if !self.iam_state.group_users.is_expanded() {
4977                        self.iam_state.group_users.expand();
4978                    }
4979                } else if self.iam_state.group_tab == GroupTab::Permissions {
4980                    if !self.iam_state.policies.is_expanded() {
4981                        self.iam_state.policies.expand();
4982                    }
4983                } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
4984                    && !self.iam_state.last_accessed_services.is_expanded()
4985                {
4986                    self.iam_state.last_accessed_services.expand();
4987                }
4988            } else if !self.iam_state.groups.is_expanded() {
4989                self.iam_state.groups.expand();
4990            }
4991        }
4992    }
4993
4994    fn go_to_page(&mut self, page: usize) {
4995        if page == 0 {
4996            return;
4997        }
4998
4999        match self.current_service {
5000            Service::CloudWatchAlarms => {
5001                let alarm_page_size = self.alarms_state.table.page_size.value();
5002                let target = (page - 1) * alarm_page_size;
5003                let filtered_count = match self.alarms_state.alarm_tab {
5004                    AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
5005                    AlarmTab::InAlarm => self
5006                        .alarms_state
5007                        .table
5008                        .items
5009                        .iter()
5010                        .filter(|a| a.state.to_uppercase() == "ALARM")
5011                        .count(),
5012                };
5013                let max_offset = filtered_count.saturating_sub(alarm_page_size);
5014                self.alarms_state.table.scroll_offset = target.min(max_offset);
5015                self.alarms_state.table.selected = self
5016                    .alarms_state
5017                    .table
5018                    .scroll_offset
5019                    .min(filtered_count.saturating_sub(1));
5020            }
5021            Service::CloudWatchLogGroups => match self.view_mode {
5022                ViewMode::Events => {
5023                    let page_size = 20;
5024                    let target = (page - 1) * page_size;
5025                    let max = self.log_groups_state.log_events.len().saturating_sub(1);
5026                    self.log_groups_state.event_scroll_offset = target.min(max);
5027                }
5028                ViewMode::Detail => {
5029                    let page_size = 20;
5030                    let target = (page - 1) * page_size;
5031                    let max = self.log_groups_state.log_streams.len().saturating_sub(1);
5032                    self.log_groups_state.selected_stream = target.min(max);
5033                }
5034                ViewMode::List => {
5035                    let total = self.log_groups_state.log_groups.items.len();
5036                    self.log_groups_state.log_groups.goto_page(page, total);
5037                }
5038                _ => {}
5039            },
5040            Service::EcrRepositories => {
5041                if self.ecr_state.current_repository.is_some() {
5042                    let filtered_count = self
5043                        .ecr_state
5044                        .images
5045                        .filtered(|img| {
5046                            self.ecr_state.images.filter.is_empty()
5047                                || img
5048                                    .tag
5049                                    .to_lowercase()
5050                                    .contains(&self.ecr_state.images.filter.to_lowercase())
5051                                || img
5052                                    .digest
5053                                    .to_lowercase()
5054                                    .contains(&self.ecr_state.images.filter.to_lowercase())
5055                        })
5056                        .len();
5057                    self.ecr_state.images.goto_page(page, filtered_count);
5058                } else {
5059                    let filtered_count = self
5060                        .ecr_state
5061                        .repositories
5062                        .filtered(|r| {
5063                            self.ecr_state.repositories.filter.is_empty()
5064                                || r.name
5065                                    .to_lowercase()
5066                                    .contains(&self.ecr_state.repositories.filter.to_lowercase())
5067                        })
5068                        .len();
5069                    self.ecr_state.repositories.goto_page(page, filtered_count);
5070                }
5071            }
5072            Service::SqsQueues => {
5073                let filtered_count = crate::ui::sqs::filtered_queues(
5074                    &self.sqs_state.queues.items,
5075                    &self.sqs_state.queues.filter,
5076                )
5077                .len();
5078                self.sqs_state.queues.goto_page(page, filtered_count);
5079            }
5080            Service::S3Buckets => {
5081                if self.s3_state.current_bucket.is_some() {
5082                    let page_size = 50; // S3 objects use fixed page size
5083                    let target = (page - 1) * page_size;
5084                    let total_rows = self.calculate_total_object_rows();
5085                    let max = total_rows.saturating_sub(1);
5086                    self.s3_state.selected_object = target.min(max);
5087                } else {
5088                    let page_size = 50; // S3 buckets use fixed page size
5089                    let target = (page - 1) * page_size;
5090                    let total_rows = self.calculate_total_bucket_rows();
5091                    let max = total_rows.saturating_sub(1);
5092                    self.s3_state.selected_row = target.min(max);
5093                }
5094            }
5095            Service::LambdaFunctions => {
5096                if self.lambda_state.current_function.is_some()
5097                    && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5098                {
5099                    let filtered_count = self
5100                        .lambda_state
5101                        .version_table
5102                        .filtered(|v| {
5103                            self.lambda_state.version_table.filter.is_empty()
5104                                || v.version.to_lowercase().contains(
5105                                    &self.lambda_state.version_table.filter.to_lowercase(),
5106                                )
5107                                || v.aliases.to_lowercase().contains(
5108                                    &self.lambda_state.version_table.filter.to_lowercase(),
5109                                )
5110                                || v.description.to_lowercase().contains(
5111                                    &self.lambda_state.version_table.filter.to_lowercase(),
5112                                )
5113                        })
5114                        .len();
5115                    self.lambda_state
5116                        .version_table
5117                        .goto_page(page, filtered_count);
5118                } else {
5119                    let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
5120                    self.lambda_state.table.goto_page(page, filtered_count);
5121                }
5122            }
5123            Service::LambdaApplications => {
5124                let filtered_count = crate::ui::lambda::filtered_lambda_applications(self).len();
5125                self.lambda_application_state
5126                    .table
5127                    .goto_page(page, filtered_count);
5128            }
5129            Service::CloudFormationStacks => {
5130                let filtered_count = self.filtered_cloudformation_stacks().len();
5131                self.cfn_state.table.goto_page(page, filtered_count);
5132            }
5133            Service::IamUsers => {
5134                let filtered_count = crate::ui::iam::filtered_iam_users(self).len();
5135                self.iam_state.users.goto_page(page, filtered_count);
5136            }
5137            Service::IamRoles => {
5138                let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
5139                self.iam_state.roles.goto_page(page, filtered_count);
5140            }
5141            _ => {}
5142        }
5143    }
5144
5145    fn prev_pane(&mut self) {
5146        if self.current_service == Service::S3Buckets {
5147            if self.s3_state.current_bucket.is_some() {
5148                // In objects view - collapse prefix or jump to parent
5149                // Map visual index to actual object (including nested items)
5150                let mut visual_idx = 0;
5151                let mut found_obj: Option<S3Object> = None;
5152                let mut parent_idx: Option<usize> = None;
5153
5154                // Helper to recursively find object and its parent
5155                #[allow(clippy::too_many_arguments)]
5156                fn find_with_parent(
5157                    objects: &[S3Object],
5158                    visual_idx: &mut usize,
5159                    target_idx: usize,
5160                    expanded_prefixes: &std::collections::HashSet<String>,
5161                    prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5162                    found_obj: &mut Option<S3Object>,
5163                    parent_idx: &mut Option<usize>,
5164                    current_parent: Option<usize>,
5165                ) {
5166                    for obj in objects {
5167                        if *visual_idx == target_idx {
5168                            *found_obj = Some(obj.clone());
5169                            *parent_idx = current_parent;
5170                            return;
5171                        }
5172                        let obj_idx = *visual_idx;
5173                        *visual_idx += 1;
5174
5175                        // Check nested items if expanded
5176                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5177                            if let Some(preview) = prefix_preview.get(&obj.key) {
5178                                find_with_parent(
5179                                    preview,
5180                                    visual_idx,
5181                                    target_idx,
5182                                    expanded_prefixes,
5183                                    prefix_preview,
5184                                    found_obj,
5185                                    parent_idx,
5186                                    Some(obj_idx),
5187                                );
5188                                if found_obj.is_some() {
5189                                    return;
5190                                }
5191                            }
5192                        }
5193                    }
5194                }
5195
5196                find_with_parent(
5197                    &self.s3_state.objects,
5198                    &mut visual_idx,
5199                    self.s3_state.selected_object,
5200                    &self.s3_state.expanded_prefixes,
5201                    &self.s3_state.prefix_preview,
5202                    &mut found_obj,
5203                    &mut parent_idx,
5204                    None,
5205                );
5206
5207                if let Some(obj) = found_obj {
5208                    if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
5209                        // Expanded: collapse it
5210                        self.s3_state.expanded_prefixes.remove(&obj.key);
5211                    } else if let Some(parent) = parent_idx {
5212                        // Already collapsed or not a prefix: jump to parent
5213                        self.s3_state.selected_object = parent;
5214                    }
5215                }
5216
5217                // Adjust scroll offset to keep selection visible
5218                let visible_rows = self.s3_state.object_visible_rows.get();
5219                if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5220                    self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5221                } else if self.s3_state.selected_object
5222                    >= self.s3_state.object_scroll_offset + visible_rows
5223                {
5224                    self.s3_state.object_scroll_offset = self
5225                        .s3_state
5226                        .selected_object
5227                        .saturating_sub(visible_rows - 1);
5228                }
5229            } else {
5230                // In bucket list - find which bucket/prefix the selected row corresponds to
5231                let mut row_idx = 0;
5232                for bucket in &self.s3_state.buckets.items {
5233                    if row_idx == self.s3_state.selected_row {
5234                        // Selected row is a bucket - collapse it
5235                        self.s3_state.expanded_prefixes.remove(&bucket.name);
5236                        break;
5237                    }
5238                    row_idx += 1;
5239                    if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5240                        if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5241                            // Recursive function to check nested items at any depth
5242                            #[allow(clippy::too_many_arguments)]
5243                            fn check_nested_collapse(
5244                                objects: &[crate::s3::Object],
5245                                row_idx: &mut usize,
5246                                target_row: usize,
5247                                expanded_prefixes: &mut std::collections::HashSet<String>,
5248                                prefix_preview: &std::collections::HashMap<
5249                                    String,
5250                                    Vec<crate::s3::Object>,
5251                                >,
5252                                found: &mut bool,
5253                                selected_row: &mut usize,
5254                                parent_row: usize,
5255                            ) {
5256                                for obj in objects {
5257                                    let current_row = *row_idx;
5258                                    if *row_idx == target_row {
5259                                        // Selected this item - collapse or jump to parent
5260                                        if obj.is_prefix {
5261                                            if expanded_prefixes.contains(&obj.key) {
5262                                                // Expanded: collapse it
5263                                                expanded_prefixes.remove(&obj.key);
5264                                            } else {
5265                                                // Already collapsed: jump to parent
5266                                                *selected_row = parent_row;
5267                                            }
5268                                        } else {
5269                                            // Not a prefix: jump to parent
5270                                            *selected_row = parent_row;
5271                                        }
5272                                        *found = true;
5273                                        return;
5274                                    }
5275                                    *row_idx += 1;
5276
5277                                    // Recursively check nested items if expanded
5278                                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5279                                        if let Some(nested) = prefix_preview.get(&obj.key) {
5280                                            check_nested_collapse(
5281                                                nested,
5282                                                row_idx,
5283                                                target_row,
5284                                                expanded_prefixes,
5285                                                prefix_preview,
5286                                                found,
5287                                                selected_row,
5288                                                current_row,
5289                                            );
5290                                            if *found {
5291                                                return;
5292                                            }
5293                                        } else {
5294                                            *row_idx += 1; // Loading row
5295                                        }
5296                                    }
5297                                }
5298                            }
5299
5300                            let mut found = false;
5301                            let parent_row = row_idx - 1; // Parent is the bucket
5302                            check_nested_collapse(
5303                                preview,
5304                                &mut row_idx,
5305                                self.s3_state.selected_row,
5306                                &mut self.s3_state.expanded_prefixes,
5307                                &self.s3_state.prefix_preview,
5308                                &mut found,
5309                                &mut self.s3_state.selected_row,
5310                                parent_row,
5311                            );
5312                            if found {
5313                                // Adjust scroll offset to keep selection visible
5314                                let visible_rows = self.s3_state.bucket_visible_rows.get();
5315                                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5316                                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5317                                } else if self.s3_state.selected_row
5318                                    >= self.s3_state.bucket_scroll_offset + visible_rows
5319                                {
5320                                    self.s3_state.bucket_scroll_offset =
5321                                        self.s3_state.selected_row.saturating_sub(visible_rows - 1);
5322                                }
5323                                return;
5324                            }
5325                        } else {
5326                            row_idx += 1;
5327                        }
5328                    }
5329                }
5330
5331                // Adjust scroll offset to keep selection visible after collapse
5332                let visible_rows = self.s3_state.bucket_visible_rows.get();
5333                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5334                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5335                } else if self.s3_state.selected_row
5336                    >= self.s3_state.bucket_scroll_offset + visible_rows
5337                {
5338                    self.s3_state.bucket_scroll_offset =
5339                        self.s3_state.selected_row.saturating_sub(visible_rows - 1);
5340                }
5341            }
5342        } else if self.view_mode == ViewMode::InsightsResults {
5343            // Left arrow scrolls horizontally by 1 column
5344            self.insights_state.insights.results_horizontal_scroll = self
5345                .insights_state
5346                .insights
5347                .results_horizontal_scroll
5348                .saturating_sub(1);
5349        } else if self.current_service == Service::CloudWatchLogGroups
5350            && self.view_mode == ViewMode::List
5351        {
5352            // Collapse expanded log group
5353            if self.log_groups_state.log_groups.has_expanded_item() {
5354                self.log_groups_state.log_groups.collapse();
5355            }
5356        } else if self.current_service == Service::CloudWatchLogGroups
5357            && self.view_mode == ViewMode::Detail
5358        {
5359            // Collapse expanded log stream
5360            if self.log_groups_state.expanded_stream.is_some() {
5361                self.log_groups_state.expanded_stream = None;
5362            }
5363        } else if self.view_mode == ViewMode::Events {
5364            // Collapse expanded event
5365            if self.log_groups_state.expanded_event.is_some() {
5366                self.log_groups_state.expanded_event = None;
5367            }
5368        } else if self.current_service == Service::CloudWatchAlarms {
5369            // Collapse expanded alarm
5370            self.alarms_state.table.collapse();
5371        } else if self.current_service == Service::EcrRepositories {
5372            if self.ecr_state.current_repository.is_some() {
5373                // In images view - collapse expanded image
5374                self.ecr_state.images.collapse();
5375            } else {
5376                // In repositories view - collapse expanded repository
5377                self.ecr_state.repositories.collapse();
5378            }
5379        } else if self.current_service == Service::SqsQueues {
5380            if self.sqs_state.current_queue.is_some()
5381                && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
5382            {
5383                self.sqs_state.triggers.collapse();
5384            } else if self.sqs_state.current_queue.is_some()
5385                && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
5386            {
5387                self.sqs_state.pipes.collapse();
5388            } else if self.sqs_state.current_queue.is_some()
5389                && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
5390            {
5391                self.sqs_state.tags.collapse();
5392            } else if self.sqs_state.current_queue.is_some()
5393                && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
5394            {
5395                self.sqs_state.subscriptions.collapse();
5396            } else {
5397                self.sqs_state.queues.collapse();
5398            }
5399        } else if self.current_service == Service::LambdaFunctions {
5400            if self.lambda_state.current_function.is_some()
5401                && self.lambda_state.detail_tab == LambdaDetailTab::Code
5402            {
5403                // Collapse selected layer
5404                self.lambda_state.layer_expanded = None;
5405            } else if self.lambda_state.current_function.is_some()
5406                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5407            {
5408                // Collapse selected version
5409                self.lambda_state.version_table.collapse();
5410            } else if self.lambda_state.current_function.is_some()
5411                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5412                    || (self.lambda_state.current_version.is_some()
5413                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
5414            {
5415                // Collapse selected alias
5416                self.lambda_state.alias_table.collapse();
5417            } else if self.lambda_state.current_function.is_none() {
5418                // Collapse expanded function
5419                self.lambda_state.table.collapse();
5420            }
5421        } else if self.current_service == Service::LambdaApplications {
5422            if self.lambda_application_state.current_application.is_some() {
5423                // In detail view - collapse resource or deployment
5424                if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
5425                {
5426                    self.lambda_application_state.resources.collapse();
5427                } else {
5428                    self.lambda_application_state.deployments.collapse();
5429                }
5430            } else {
5431                // Collapse expanded application in list
5432                if self.lambda_application_state.table.has_expanded_item() {
5433                    self.lambda_application_state.table.collapse();
5434                }
5435            }
5436        } else if self.current_service == Service::CloudFormationStacks
5437            && self.cfn_state.current_stack.is_none()
5438        {
5439            self.cfn_state.table.collapse();
5440        } else if self.current_service == Service::IamUsers {
5441            if self.iam_state.users.has_expanded_item() {
5442                self.iam_state.users.collapse();
5443            }
5444        } else if self.current_service == Service::IamRoles {
5445            if self.view_mode == ViewMode::PolicyView {
5446                // Go back from policy view to role detail
5447                self.view_mode = ViewMode::Detail;
5448                self.iam_state.current_policy = None;
5449                self.iam_state.policy_document.clear();
5450                self.iam_state.policy_scroll = 0;
5451            } else if self.iam_state.current_role.is_some() {
5452                if self.iam_state.role_tab == RoleTab::Tags
5453                    && self.iam_state.tags.has_expanded_item()
5454                {
5455                    self.iam_state.tags.collapse();
5456                } else if self.iam_state.role_tab == RoleTab::LastAccessed
5457                    && self
5458                        .iam_state
5459                        .last_accessed_services
5460                        .expanded_item
5461                        .is_some()
5462                {
5463                    self.iam_state.last_accessed_services.collapse();
5464                } else if self.iam_state.policies.has_expanded_item() {
5465                    self.iam_state.policies.collapse();
5466                }
5467            } else if self.iam_state.roles.has_expanded_item() {
5468                self.iam_state.roles.collapse();
5469            }
5470        } else if self.current_service == Service::IamUserGroups {
5471            if self.iam_state.current_group.is_some() {
5472                if self.iam_state.group_tab == GroupTab::Users
5473                    && self.iam_state.group_users.has_expanded_item()
5474                {
5475                    self.iam_state.group_users.collapse();
5476                } else if self.iam_state.group_tab == GroupTab::Permissions
5477                    && self.iam_state.policies.has_expanded_item()
5478                {
5479                    self.iam_state.policies.collapse();
5480                } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
5481                    && self
5482                        .iam_state
5483                        .last_accessed_services
5484                        .expanded_item
5485                        .is_some()
5486                {
5487                    self.iam_state.last_accessed_services.collapse();
5488                }
5489            } else if self.iam_state.groups.has_expanded_item() {
5490                self.iam_state.groups.collapse();
5491            }
5492        }
5493    }
5494
5495    fn select_item(&mut self) {
5496        if self.mode == Mode::RegionPicker {
5497            let filtered = self.get_filtered_regions();
5498            if let Some(region) = filtered.get(self.region_picker_selected) {
5499                // Save current session before changing region
5500                if !self.tabs.is_empty() {
5501                    let mut session = Session::new(
5502                        self.profile.clone(),
5503                        self.region.clone(),
5504                        self.config.account_id.clone(),
5505                        self.config.role_arn.clone(),
5506                    );
5507
5508                    for tab in &self.tabs {
5509                        session.tabs.push(SessionTab {
5510                            service: format!("{:?}", tab.service),
5511                            title: tab.title.clone(),
5512                            breadcrumb: tab.breadcrumb.clone(),
5513                            filter: None,
5514                            selected_item: None,
5515                        });
5516                    }
5517
5518                    let _ = session.save();
5519                }
5520
5521                self.region = region.code.to_string();
5522                self.config.region = region.code.to_string();
5523
5524                // Close all tabs - region change invalidates all data
5525                self.tabs.clear();
5526                self.current_tab = 0;
5527                self.service_selected = false;
5528
5529                self.mode = Mode::Normal;
5530            }
5531        } else if self.mode == Mode::ProfilePicker {
5532            let filtered = self.get_filtered_profiles();
5533            if let Some(profile) = filtered.get(self.profile_picker_selected) {
5534                let profile_name = profile.name.clone();
5535                let profile_region = profile.region.clone();
5536
5537                self.profile = profile_name.clone();
5538                std::env::set_var("AWS_PROFILE", &profile_name);
5539
5540                // Use profile's region if available
5541                if let Some(region) = profile_region {
5542                    self.region = region;
5543                }
5544
5545                self.mode = Mode::Normal;
5546                // Note: Changing profile requires reconnecting to AWS
5547            }
5548        } else if self.mode == Mode::ServicePicker {
5549            let filtered = self.filtered_services();
5550            if let Some(&service) = filtered.get(self.service_picker.selected) {
5551                let new_service = match service {
5552                    "CloudWatch > Log Groups" => Service::CloudWatchLogGroups,
5553                    "CloudWatch > Logs Insights" => Service::CloudWatchInsights,
5554                    "CloudWatch > Alarms" => Service::CloudWatchAlarms,
5555                    "CloudFormation > Stacks" => Service::CloudFormationStacks,
5556                    "ECR > Repositories" => Service::EcrRepositories,
5557                    "IAM > Users" => Service::IamUsers,
5558                    "IAM > Roles" => Service::IamRoles,
5559                    "IAM > User Groups" => Service::IamUserGroups,
5560                    "Lambda > Functions" => Service::LambdaFunctions,
5561                    "Lambda > Applications" => Service::LambdaApplications,
5562                    "S3 > Buckets" => Service::S3Buckets,
5563                    "SQS > Queues" => Service::SqsQueues,
5564                    _ => return,
5565                };
5566
5567                // Create new tab
5568                self.tabs.push(Tab {
5569                    service: new_service,
5570                    title: service.to_string(),
5571                    breadcrumb: service.to_string(),
5572                });
5573                self.current_tab = self.tabs.len() - 1;
5574                self.current_service = new_service;
5575                self.view_mode = ViewMode::List;
5576                self.service_selected = true;
5577                self.mode = Mode::Normal;
5578            }
5579        } else if self.mode == Mode::TabPicker {
5580            let filtered = self.get_filtered_tabs();
5581            if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
5582                self.current_tab = idx;
5583                self.current_service = self.tabs[idx].service;
5584                self.mode = Mode::Normal;
5585                self.tab_filter.clear();
5586            }
5587        } else if self.mode == Mode::SessionPicker {
5588            let filtered = self.get_filtered_sessions();
5589            if let Some(&session) = filtered.get(self.session_picker_selected) {
5590                let session = session.clone();
5591
5592                // Load the selected session
5593                self.current_session = Some(session.clone());
5594                self.profile = session.profile.clone();
5595                self.region = session.region.clone();
5596                self.config.region = session.region.clone();
5597                self.config.account_id = session.account_id.clone();
5598                self.config.role_arn = session.role_arn.clone();
5599
5600                // Restore tabs
5601                self.tabs = session
5602                    .tabs
5603                    .iter()
5604                    .map(|st| Tab {
5605                        service: match st.service.as_str() {
5606                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
5607                            "CloudWatchInsights" => Service::CloudWatchInsights,
5608                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
5609                            "S3Buckets" => Service::S3Buckets,
5610                            "SqsQueues" => Service::SqsQueues,
5611                            _ => Service::CloudWatchLogGroups,
5612                        },
5613                        title: st.title.clone(),
5614                        breadcrumb: st.breadcrumb.clone(),
5615                    })
5616                    .collect();
5617
5618                if !self.tabs.is_empty() {
5619                    self.current_tab = 0;
5620                    self.current_service = self.tabs[0].service;
5621                    self.service_selected = true;
5622                }
5623
5624                self.mode = Mode::Normal;
5625            }
5626        } else if self.mode == Mode::InsightsInput {
5627            // In InsightsInput mode, behavior depends on focus
5628            use crate::app::InsightsFocus;
5629            match self.insights_state.insights.insights_focus {
5630                InsightsFocus::Query => {
5631                    // Add newline to query
5632                    self.insights_state.insights.query_text.push('\n');
5633                    self.insights_state.insights.query_cursor_line += 1;
5634                    self.insights_state.insights.query_cursor_col = 0;
5635                }
5636                InsightsFocus::LogGroupSearch => {
5637                    // Toggle dropdown
5638                    self.insights_state.insights.show_dropdown =
5639                        !self.insights_state.insights.show_dropdown;
5640                }
5641                _ => {}
5642            }
5643        } else if self.mode == Mode::Normal {
5644            // If no service selected, select from service picker
5645            if !self.service_selected {
5646                let filtered = self.filtered_services();
5647                if let Some(&service) = filtered.get(self.service_picker.selected) {
5648                    match service {
5649                        "CloudWatch > Log Groups" => {
5650                            self.current_service = Service::CloudWatchLogGroups;
5651                            self.view_mode = ViewMode::List;
5652                            self.service_selected = true;
5653                        }
5654                        "CloudWatch > Logs Insights" => {
5655                            self.current_service = Service::CloudWatchInsights;
5656                            self.view_mode = ViewMode::InsightsResults;
5657                            self.service_selected = true;
5658                        }
5659                        "CloudWatch > Alarms" => {
5660                            self.current_service = Service::CloudWatchAlarms;
5661                            self.view_mode = ViewMode::List;
5662                            self.service_selected = true;
5663                        }
5664                        "S3 > Buckets" => {
5665                            self.current_service = Service::S3Buckets;
5666                            self.view_mode = ViewMode::List;
5667                            self.service_selected = true;
5668                        }
5669                        "ECR > Repositories" => {
5670                            self.current_service = Service::EcrRepositories;
5671                            self.view_mode = ViewMode::List;
5672                            self.service_selected = true;
5673                        }
5674                        "Lambda > Functions" => {
5675                            self.current_service = Service::LambdaFunctions;
5676                            self.view_mode = ViewMode::List;
5677                            self.service_selected = true;
5678                        }
5679                        "Lambda > Applications" => {
5680                            self.current_service = Service::LambdaApplications;
5681                            self.view_mode = ViewMode::List;
5682                            self.service_selected = true;
5683                        }
5684                        _ => {}
5685                    }
5686                }
5687                return;
5688            }
5689
5690            // Select in content area
5691            if self.view_mode == ViewMode::InsightsResults {
5692                // Toggle expand for selected result
5693                if self.insights_state.insights.expanded_result
5694                    == Some(self.insights_state.insights.results_selected)
5695                {
5696                    self.insights_state.insights.expanded_result = None;
5697                } else {
5698                    self.insights_state.insights.expanded_result =
5699                        Some(self.insights_state.insights.results_selected);
5700                }
5701            } else if self.current_service == Service::S3Buckets {
5702                if self.s3_state.current_bucket.is_none() {
5703                    // Find which bucket/prefix the selected row corresponds to
5704                    let mut row_idx = 0;
5705                    for bucket in &self.s3_state.buckets.items {
5706                        if row_idx == self.s3_state.selected_row {
5707                            // Selected a bucket - drill into it
5708                            self.s3_state.current_bucket = Some(bucket.name.clone());
5709                            self.s3_state.prefix_stack.clear();
5710                            self.s3_state.buckets.loading = true;
5711                            return;
5712                        }
5713                        row_idx += 1;
5714
5715                        // Skip error rows - they're not selectable
5716                        if self.s3_state.bucket_errors.contains_key(&bucket.name)
5717                            && self.s3_state.expanded_prefixes.contains(&bucket.name)
5718                        {
5719                            continue;
5720                        }
5721
5722                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5723                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5724                                for obj in preview {
5725                                    if row_idx == self.s3_state.selected_row {
5726                                        // Selected a prefix - drill into bucket with this prefix
5727                                        if obj.is_prefix {
5728                                            self.s3_state.current_bucket =
5729                                                Some(bucket.name.clone());
5730                                            self.s3_state.prefix_stack = vec![obj.key.clone()];
5731                                            self.s3_state.buckets.loading = true;
5732                                        }
5733                                        return;
5734                                    }
5735                                    row_idx += 1;
5736
5737                                    // Check nested preview rows
5738                                    if obj.is_prefix
5739                                        && self.s3_state.expanded_prefixes.contains(&obj.key)
5740                                    {
5741                                        if let Some(nested) =
5742                                            self.s3_state.prefix_preview.get(&obj.key)
5743                                        {
5744                                            for nested_obj in nested {
5745                                                if row_idx == self.s3_state.selected_row {
5746                                                    // Selected a nested prefix - drill into bucket with this prefix
5747                                                    if nested_obj.is_prefix {
5748                                                        self.s3_state.current_bucket =
5749                                                            Some(bucket.name.clone());
5750                                                        // Build proper prefix stack: parent, then child
5751                                                        self.s3_state.prefix_stack = vec![
5752                                                            obj.key.clone(),
5753                                                            nested_obj.key.clone(),
5754                                                        ];
5755                                                        self.s3_state.buckets.loading = true;
5756                                                    }
5757                                                    return;
5758                                                }
5759                                                row_idx += 1;
5760                                            }
5761                                        } else {
5762                                            row_idx += 1;
5763                                        }
5764                                    }
5765                                }
5766                            } else {
5767                                row_idx += 1;
5768                            }
5769                        }
5770                    }
5771                } else {
5772                    // In objects view - map visual index to actual object (including nested items)
5773                    let mut visual_idx = 0;
5774                    let mut found_obj: Option<S3Object> = None;
5775
5776                    // Helper to recursively check nested items
5777                    fn check_nested_select(
5778                        obj: &S3Object,
5779                        visual_idx: &mut usize,
5780                        target_idx: usize,
5781                        expanded_prefixes: &std::collections::HashSet<String>,
5782                        prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5783                        found_obj: &mut Option<S3Object>,
5784                    ) {
5785                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5786                            if let Some(preview) = prefix_preview.get(&obj.key) {
5787                                for nested_obj in preview {
5788                                    if *visual_idx == target_idx {
5789                                        *found_obj = Some(nested_obj.clone());
5790                                        return;
5791                                    }
5792                                    *visual_idx += 1;
5793
5794                                    // Recursively check deeper levels
5795                                    check_nested_select(
5796                                        nested_obj,
5797                                        visual_idx,
5798                                        target_idx,
5799                                        expanded_prefixes,
5800                                        prefix_preview,
5801                                        found_obj,
5802                                    );
5803                                    if found_obj.is_some() {
5804                                        return;
5805                                    }
5806                                }
5807                            } else {
5808                                // Loading row
5809                                *visual_idx += 1;
5810                            }
5811                        }
5812                    }
5813
5814                    for obj in &self.s3_state.objects {
5815                        if visual_idx == self.s3_state.selected_object {
5816                            found_obj = Some(obj.clone());
5817                            break;
5818                        }
5819                        visual_idx += 1;
5820
5821                        // Check nested items recursively
5822                        check_nested_select(
5823                            obj,
5824                            &mut visual_idx,
5825                            self.s3_state.selected_object,
5826                            &self.s3_state.expanded_prefixes,
5827                            &self.s3_state.prefix_preview,
5828                            &mut found_obj,
5829                        );
5830                        if found_obj.is_some() {
5831                            break;
5832                        }
5833                    }
5834
5835                    if let Some(obj) = found_obj {
5836                        if obj.is_prefix {
5837                            // Drill into prefix
5838                            self.s3_state.prefix_stack.push(obj.key.clone());
5839                            self.s3_state.buckets.loading = true;
5840                        }
5841                    }
5842                }
5843            } else if self.current_service == Service::CloudFormationStacks {
5844                if self.cfn_state.current_stack.is_none() {
5845                    // Drill into stack detail view
5846                    let filtered_stacks = self.filtered_cloudformation_stacks();
5847                    if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
5848                        self.cfn_state.current_stack = Some(stack.name.clone());
5849                        self.update_current_tab_breadcrumb();
5850                    }
5851                }
5852            } else if self.current_service == Service::EcrRepositories {
5853                if self.ecr_state.current_repository.is_none() {
5854                    // In repositories view - drill into selected repository
5855                    let filtered_repos = self.filtered_ecr_repositories();
5856                    if let Some(repo) = self.ecr_state.repositories.get_selected(&filtered_repos) {
5857                        let repo_name = repo.name.clone();
5858                        let repo_uri = repo.uri.clone();
5859                        self.ecr_state.current_repository = Some(repo_name);
5860                        self.ecr_state.current_repository_uri = Some(repo_uri);
5861                        self.ecr_state.images.reset();
5862                        self.ecr_state.repositories.loading = true;
5863                    }
5864                }
5865            } else if self.current_service == Service::SqsQueues {
5866                if self.sqs_state.current_queue.is_none() {
5867                    let filtered_queues = crate::ui::sqs::filtered_queues(
5868                        &self.sqs_state.queues.items,
5869                        &self.sqs_state.queues.filter,
5870                    );
5871                    if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
5872                        self.sqs_state.current_queue = Some(queue.url.clone());
5873
5874                        if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5875                            self.sqs_state.metrics_loading = true;
5876                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
5877                            self.sqs_state.triggers.loading = true;
5878                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
5879                            self.sqs_state.pipes.loading = true;
5880                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
5881                            self.sqs_state.tags.loading = true;
5882                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
5883                            self.sqs_state.subscriptions.loading = true;
5884                        }
5885                    }
5886                }
5887            } else if self.current_service == Service::IamUsers {
5888                if self.iam_state.current_user.is_some() {
5889                    // Open policy view (but not on Tags tab)
5890                    if self.iam_state.user_tab != UserTab::Tags {
5891                        let filtered = crate::ui::iam::filtered_iam_policies(self);
5892                        if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
5893                            self.iam_state.current_policy = Some(policy.policy_name.clone());
5894                            self.iam_state.policy_scroll = 0;
5895                            self.view_mode = ViewMode::PolicyView;
5896                            self.iam_state.policies.loading = true;
5897                            self.update_current_tab_breadcrumb();
5898                        }
5899                    }
5900                } else if self.iam_state.current_user.is_none() {
5901                    let filtered_users = crate::ui::iam::filtered_iam_users(self);
5902                    if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
5903                        self.iam_state.current_user = Some(user.user_name.clone());
5904                        self.iam_state.user_tab = UserTab::Permissions;
5905                        self.iam_state.policies.reset();
5906                        self.update_current_tab_breadcrumb();
5907                    }
5908                }
5909            } else if self.current_service == Service::IamRoles {
5910                if self.iam_state.current_role.is_some() {
5911                    // Open policy view (but not on Tags tab)
5912                    if self.iam_state.role_tab != RoleTab::Tags {
5913                        let filtered = crate::ui::iam::filtered_iam_policies(self);
5914                        if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
5915                            self.iam_state.current_policy = Some(policy.policy_name.clone());
5916                            self.iam_state.policy_scroll = 0;
5917                            self.view_mode = ViewMode::PolicyView;
5918                            self.iam_state.policies.loading = true;
5919                            self.update_current_tab_breadcrumb();
5920                        }
5921                    }
5922                } else if self.iam_state.current_role.is_none() {
5923                    let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
5924                    if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
5925                        self.iam_state.current_role = Some(role.role_name.clone());
5926                        self.iam_state.role_tab = RoleTab::Permissions;
5927                        self.iam_state.policies.reset();
5928                        self.update_current_tab_breadcrumb();
5929                    }
5930                }
5931            } else if self.current_service == Service::IamUserGroups {
5932                if self.iam_state.current_group.is_none() {
5933                    let filtered_groups: Vec<_> = self
5934                        .iam_state
5935                        .groups
5936                        .items
5937                        .iter()
5938                        .filter(|g| {
5939                            if self.iam_state.groups.filter.is_empty() {
5940                                true
5941                            } else {
5942                                g.group_name
5943                                    .to_lowercase()
5944                                    .contains(&self.iam_state.groups.filter.to_lowercase())
5945                            }
5946                        })
5947                        .collect();
5948                    if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
5949                        self.iam_state.current_group = Some(group.group_name.clone());
5950                        self.update_current_tab_breadcrumb();
5951                    }
5952                }
5953            } else if self.current_service == Service::LambdaFunctions {
5954                if self.lambda_state.current_function.is_some()
5955                    && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5956                {
5957                    // In Normal mode, select version to open detail view
5958                    // In other modes (FilterInput), toggle expansion
5959                    if self.mode == Mode::Normal {
5960                        let page_size = self.lambda_state.version_table.page_size.value();
5961                        let filtered: Vec<_> = self
5962                            .lambda_state
5963                            .version_table
5964                            .items
5965                            .iter()
5966                            .filter(|v| {
5967                                self.lambda_state.version_table.filter.is_empty()
5968                                    || v.version.to_lowercase().contains(
5969                                        &self.lambda_state.version_table.filter.to_lowercase(),
5970                                    )
5971                                    || v.aliases.to_lowercase().contains(
5972                                        &self.lambda_state.version_table.filter.to_lowercase(),
5973                                    )
5974                            })
5975                            .collect();
5976                        let current_page = self.lambda_state.version_table.selected / page_size;
5977                        let start_idx = current_page * page_size;
5978                        let end_idx = (start_idx + page_size).min(filtered.len());
5979                        let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
5980                        let page_index = self.lambda_state.version_table.selected % page_size;
5981                        if let Some(version) = paginated.get(page_index) {
5982                            self.lambda_state.current_version = Some(version.version.clone());
5983                            self.lambda_state.detail_tab = LambdaDetailTab::Code;
5984                        }
5985                    } else {
5986                        // Toggle expansion
5987                        if self.lambda_state.version_table.expanded_item
5988                            == Some(self.lambda_state.version_table.selected)
5989                        {
5990                            self.lambda_state.version_table.collapse();
5991                        } else {
5992                            self.lambda_state.version_table.expanded_item =
5993                                Some(self.lambda_state.version_table.selected);
5994                        }
5995                    }
5996                } else if self.lambda_state.current_function.is_some()
5997                    && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5998                {
5999                    // Select alias to view detail (no tab change - alias view has no tabs)
6000                    let filtered: Vec<_> = self
6001                        .lambda_state
6002                        .alias_table
6003                        .items
6004                        .iter()
6005                        .filter(|a| {
6006                            self.lambda_state.alias_table.filter.is_empty()
6007                                || a.name
6008                                    .to_lowercase()
6009                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6010                                || a.versions
6011                                    .to_lowercase()
6012                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6013                        })
6014                        .collect();
6015                    if let Some(alias) = self.lambda_state.alias_table.get_selected(&filtered) {
6016                        self.lambda_state.current_alias = Some(alias.name.clone());
6017                    }
6018                } else if self.lambda_state.current_function.is_none() {
6019                    let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
6020                    if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
6021                        self.lambda_state.current_function = Some(func.name.clone());
6022                        self.lambda_state.detail_tab = LambdaDetailTab::Code;
6023                        self.update_current_tab_breadcrumb();
6024                    }
6025                }
6026            } else if self.current_service == Service::LambdaApplications {
6027                let filtered = crate::ui::lambda::filtered_lambda_applications(self);
6028                if let Some(app) = self.lambda_application_state.table.get_selected(&filtered) {
6029                    let app_name = app.name.clone();
6030                    self.lambda_application_state.current_application = Some(app_name.clone());
6031                    self.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Overview;
6032
6033                    // Load mock resources
6034                    use crate::lambda::Resource;
6035                    self.lambda_application_state.resources.items = vec![
6036                        Resource {
6037                            logical_id: "ApiGatewayRestApi".to_string(),
6038                            physical_id: "abc123xyz".to_string(),
6039                            resource_type: "AWS::ApiGateway::RestApi".to_string(),
6040                            last_modified: "2025-01-10 14:30:00 (UTC)".to_string(),
6041                        },
6042                        Resource {
6043                            logical_id: "LambdaFunction".to_string(),
6044                            physical_id: format!("{}-function", app_name),
6045                            resource_type: "AWS::Lambda::Function".to_string(),
6046                            last_modified: "2025-01-10 14:25:00 (UTC)".to_string(),
6047                        },
6048                        Resource {
6049                            logical_id: "DynamoDBTable".to_string(),
6050                            physical_id: format!("{}-table", app_name),
6051                            resource_type: "AWS::DynamoDB::Table".to_string(),
6052                            last_modified: "2025-01-09 10:15:00 (UTC)".to_string(),
6053                        },
6054                    ];
6055
6056                    // Load mock deployments
6057                    use crate::lambda::Deployment;
6058                    self.lambda_application_state.deployments.items = vec![
6059                        Deployment {
6060                            deployment_id: "d-ABC123XYZ".to_string(),
6061                            resource_type: "AWS::Serverless::Application".to_string(),
6062                            last_updated: "2025-01-10 14:30:00 (UTC)".to_string(),
6063                            status: "Succeeded".to_string(),
6064                        },
6065                        Deployment {
6066                            deployment_id: "d-DEF456UVW".to_string(),
6067                            resource_type: "AWS::Serverless::Application".to_string(),
6068                            last_updated: "2025-01-09 10:15:00 (UTC)".to_string(),
6069                            status: "Succeeded".to_string(),
6070                        },
6071                    ];
6072
6073                    self.update_current_tab_breadcrumb();
6074                }
6075            } else if self.current_service == Service::CloudWatchLogGroups {
6076                if self.view_mode == ViewMode::List {
6077                    // Map filtered selection to actual group index
6078                    let filtered_groups = self.filtered_log_groups();
6079                    if let Some(selected_group) =
6080                        filtered_groups.get(self.log_groups_state.log_groups.selected)
6081                    {
6082                        if let Some(actual_idx) = self
6083                            .log_groups_state
6084                            .log_groups
6085                            .items
6086                            .iter()
6087                            .position(|g| g.name == selected_group.name)
6088                        {
6089                            self.log_groups_state.log_groups.selected = actual_idx;
6090                        }
6091                    }
6092                    self.view_mode = ViewMode::Detail;
6093                    self.log_groups_state.log_streams.clear();
6094                    self.log_groups_state.selected_stream = 0;
6095                    self.log_groups_state.loading = true;
6096                    self.update_current_tab_breadcrumb();
6097                } else if self.view_mode == ViewMode::Detail {
6098                    // Map filtered stream selection to actual stream index
6099                    let filtered_streams = self.filtered_log_streams();
6100                    if let Some(selected_stream) =
6101                        filtered_streams.get(self.log_groups_state.selected_stream)
6102                    {
6103                        if let Some(actual_idx) = self
6104                            .log_groups_state
6105                            .log_streams
6106                            .iter()
6107                            .position(|s| s.name == selected_stream.name)
6108                        {
6109                            self.log_groups_state.selected_stream = actual_idx;
6110                        }
6111                    }
6112                    self.view_mode = ViewMode::Events;
6113                    self.update_current_tab_breadcrumb();
6114                    self.log_groups_state.log_events.clear();
6115                    self.log_groups_state.event_scroll_offset = 0;
6116                    self.log_groups_state.next_backward_token = None;
6117                    self.log_groups_state.loading = true;
6118                } else if self.view_mode == ViewMode::Events {
6119                    // Toggle expand for selected event
6120                    if self.log_groups_state.expanded_event
6121                        == Some(self.log_groups_state.event_scroll_offset)
6122                    {
6123                        self.log_groups_state.expanded_event = None;
6124                    } else {
6125                        self.log_groups_state.expanded_event =
6126                            Some(self.log_groups_state.event_scroll_offset);
6127                    }
6128                }
6129            } else if self.current_service == Service::CloudWatchAlarms {
6130                // Toggle expand for selected alarm
6131                self.alarms_state.table.toggle_expand();
6132            } else if self.current_service == Service::CloudWatchInsights {
6133                // In Normal mode, Enter always executes query
6134                if !self.insights_state.insights.selected_log_groups.is_empty() {
6135                    self.log_groups_state.loading = true;
6136                    self.insights_state.insights.query_completed = true;
6137                }
6138            }
6139        }
6140    }
6141
6142    pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
6143        self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
6144        Ok(())
6145    }
6146
6147    pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
6148        let alarms = self.alarms_client.list_alarms().await?;
6149        self.alarms_state.table.items = alarms
6150            .into_iter()
6151            .map(
6152                |(
6153                    name,
6154                    state,
6155                    state_updated,
6156                    description,
6157                    metric_name,
6158                    namespace,
6159                    statistic,
6160                    period,
6161                    comparison,
6162                    threshold,
6163                    actions_enabled,
6164                    state_reason,
6165                    resource,
6166                    dimensions,
6167                    expression,
6168                    alarm_type,
6169                    cross_account,
6170                )| Alarm {
6171                    name,
6172                    state,
6173                    state_updated_timestamp: state_updated,
6174                    description,
6175                    metric_name,
6176                    namespace,
6177                    statistic,
6178                    period,
6179                    comparison_operator: comparison,
6180                    threshold,
6181                    actions_enabled,
6182                    state_reason,
6183                    resource,
6184                    dimensions,
6185                    expression,
6186                    alarm_type,
6187                    cross_account,
6188                },
6189            )
6190            .collect();
6191        Ok(())
6192    }
6193
6194    pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
6195        if let Some(bucket_name) = &self.s3_state.current_bucket {
6196            // Get or fetch bucket region
6197            let bucket_region = if let Some(bucket) = self
6198                .s3_state
6199                .buckets
6200                .items
6201                .iter_mut()
6202                .find(|b| &b.name == bucket_name)
6203            {
6204                if bucket.region.is_empty() {
6205                    // Fetch the actual region
6206                    let region = self.s3_client.get_bucket_location(bucket_name).await?;
6207                    bucket.region = region.clone();
6208                    region
6209                } else {
6210                    bucket.region.clone()
6211                }
6212            } else {
6213                self.config.region.clone()
6214            };
6215
6216            let prefix = self
6217                .s3_state
6218                .prefix_stack
6219                .last()
6220                .cloned()
6221                .unwrap_or_default();
6222            let objects = self
6223                .s3_client
6224                .list_objects(bucket_name, &bucket_region, &prefix)
6225                .await?;
6226            self.s3_state.objects = objects
6227                .into_iter()
6228                .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
6229                    key,
6230                    size,
6231                    last_modified: modified,
6232                    is_prefix,
6233                    storage_class,
6234                })
6235                .collect();
6236            self.s3_state.selected_object = 0;
6237        }
6238        Ok(())
6239    }
6240
6241    pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
6242        let bucket_region = self
6243            .s3_state
6244            .buckets
6245            .items
6246            .iter()
6247            .find(|b| b.name == bucket_name)
6248            .and_then(|b| {
6249                if b.region.is_empty() {
6250                    None
6251                } else {
6252                    Some(b.region.as_str())
6253                }
6254            })
6255            .unwrap_or(self.config.region.as_str());
6256        let objects = self
6257            .s3_client
6258            .list_objects(&bucket_name, bucket_region, "")
6259            .await?;
6260        let preview: Vec<S3Object> = objects
6261            .into_iter()
6262            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
6263                key,
6264                size,
6265                last_modified: modified,
6266                is_prefix,
6267                storage_class,
6268            })
6269            .collect();
6270        self.s3_state.bucket_preview.insert(bucket_name, preview);
6271        Ok(())
6272    }
6273
6274    pub async fn load_prefix_preview(
6275        &mut self,
6276        bucket_name: String,
6277        prefix: String,
6278    ) -> anyhow::Result<()> {
6279        let bucket_region = self
6280            .s3_state
6281            .buckets
6282            .items
6283            .iter()
6284            .find(|b| b.name == bucket_name)
6285            .and_then(|b| {
6286                if b.region.is_empty() {
6287                    None
6288                } else {
6289                    Some(b.region.as_str())
6290                }
6291            })
6292            .unwrap_or(self.config.region.as_str());
6293        let objects = self
6294            .s3_client
6295            .list_objects(&bucket_name, bucket_region, &prefix)
6296            .await?;
6297        let preview: Vec<S3Object> = objects
6298            .into_iter()
6299            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
6300                key,
6301                size,
6302                last_modified: modified,
6303                is_prefix,
6304                storage_class,
6305            })
6306            .collect();
6307        self.s3_state.prefix_preview.insert(prefix, preview);
6308        Ok(())
6309    }
6310
6311    pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
6312        let repos = match self.ecr_state.tab {
6313            EcrTab::Private => self.ecr_client.list_private_repositories().await?,
6314            EcrTab::Public => self.ecr_client.list_public_repositories().await?,
6315        };
6316
6317        self.ecr_state.repositories.items = repos
6318            .into_iter()
6319            .map(|r| EcrRepository {
6320                name: r.name,
6321                uri: r.uri,
6322                created_at: r.created_at,
6323                tag_immutability: r.tag_immutability,
6324                encryption_type: r.encryption_type,
6325            })
6326            .collect();
6327
6328        self.ecr_state
6329            .repositories
6330            .items
6331            .sort_by(|a, b| a.name.cmp(&b.name));
6332        Ok(())
6333    }
6334
6335    pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
6336        if let Some(repo_name) = &self.ecr_state.current_repository {
6337            if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
6338                let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
6339
6340                self.ecr_state.images.items = images
6341                    .into_iter()
6342                    .map(|i| EcrImage {
6343                        tag: i.tag,
6344                        artifact_type: i.artifact_type,
6345                        pushed_at: i.pushed_at,
6346                        size_bytes: i.size_bytes,
6347                        uri: i.uri,
6348                        digest: i.digest,
6349                        last_pull_time: i.last_pull_time,
6350                    })
6351                    .collect();
6352
6353                self.ecr_state
6354                    .images
6355                    .items
6356                    .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
6357            }
6358        }
6359        Ok(())
6360    }
6361
6362    pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
6363        let stacks = self
6364            .cloudformation_client
6365            .list_stacks(self.cfn_state.view_nested)
6366            .await?;
6367
6368        let mut stacks: Vec<CfnStack> = stacks
6369            .into_iter()
6370            .map(|s| CfnStack {
6371                name: s.name,
6372                stack_id: s.stack_id,
6373                status: s.status,
6374                created_time: s.created_time,
6375                updated_time: s.updated_time,
6376                deleted_time: s.deleted_time,
6377                drift_status: s.drift_status,
6378                last_drift_check_time: s.last_drift_check_time,
6379                status_reason: s.status_reason,
6380                description: s.description,
6381                detailed_status: String::new(),
6382                root_stack: String::new(),
6383                parent_stack: String::new(),
6384                termination_protection: false,
6385                iam_role: String::new(),
6386                tags: Vec::new(),
6387                stack_policy: String::new(),
6388                rollback_monitoring_time: String::new(),
6389                rollback_alarms: Vec::new(),
6390                notification_arns: Vec::new(),
6391            })
6392            .collect();
6393
6394        // Sort by created_time DESC
6395        stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
6396
6397        self.cfn_state.table.items = stacks;
6398
6399        Ok(())
6400    }
6401
6402    pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
6403        // Load attached (managed) policies
6404        let attached_policies = self
6405            .iam_client
6406            .list_attached_role_policies(role_name)
6407            .await
6408            .map_err(|e| anyhow::anyhow!(e))?;
6409
6410        let mut policies: Vec<crate::iam::Policy> = attached_policies
6411            .into_iter()
6412            .map(|p| crate::iam::Policy {
6413                policy_name: p.policy_name().unwrap_or("").to_string(),
6414                policy_type: "Managed".to_string(),
6415                attached_via: "Direct".to_string(),
6416                attached_entities: "-".to_string(),
6417                description: "-".to_string(),
6418                creation_time: "-".to_string(),
6419                edited_time: "-".to_string(),
6420                policy_arn: p.policy_arn().map(|s| s.to_string()),
6421            })
6422            .collect();
6423
6424        // Load inline policies
6425        let inline_policy_names = self
6426            .iam_client
6427            .list_role_policies(role_name)
6428            .await
6429            .map_err(|e| anyhow::anyhow!(e))?;
6430
6431        for policy_name in inline_policy_names {
6432            policies.push(crate::iam::Policy {
6433                policy_name,
6434                policy_type: "Inline".to_string(),
6435                attached_via: "Direct".to_string(),
6436                attached_entities: "-".to_string(),
6437                description: "-".to_string(),
6438                creation_time: "-".to_string(),
6439                edited_time: "-".to_string(),
6440                policy_arn: None,
6441            });
6442        }
6443
6444        self.iam_state.policies.items = policies;
6445
6446        Ok(())
6447    }
6448
6449    pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
6450        let attached_policies = self
6451            .iam_client
6452            .list_attached_group_policies(group_name)
6453            .await
6454            .map_err(|e| anyhow::anyhow!(e))?;
6455
6456        let mut policies: Vec<crate::iam::Policy> = attached_policies
6457            .into_iter()
6458            .map(|p| crate::iam::Policy {
6459                policy_name: p.policy_name().unwrap_or("").to_string(),
6460                policy_type: "AWS managed".to_string(),
6461                attached_via: "Direct".to_string(),
6462                attached_entities: "-".to_string(),
6463                description: "-".to_string(),
6464                creation_time: "-".to_string(),
6465                edited_time: "-".to_string(),
6466                policy_arn: p.policy_arn().map(|s| s.to_string()),
6467            })
6468            .collect();
6469
6470        let inline_policy_names = self
6471            .iam_client
6472            .list_group_policies(group_name)
6473            .await
6474            .map_err(|e| anyhow::anyhow!(e))?;
6475
6476        for policy_name in inline_policy_names {
6477            policies.push(crate::iam::Policy {
6478                policy_name,
6479                policy_type: "Inline".to_string(),
6480                attached_via: "Direct".to_string(),
6481                attached_entities: "-".to_string(),
6482                description: "-".to_string(),
6483                creation_time: "-".to_string(),
6484                edited_time: "-".to_string(),
6485                policy_arn: None,
6486            });
6487        }
6488
6489        self.iam_state.policies.items = policies;
6490
6491        Ok(())
6492    }
6493
6494    pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
6495        let users = self
6496            .iam_client
6497            .get_group_users(group_name)
6498            .await
6499            .map_err(|e| anyhow::anyhow!(e))?;
6500
6501        let group_users: Vec<crate::iam::GroupUser> = users
6502            .into_iter()
6503            .map(|u| {
6504                let creation_time = {
6505                    let dt = u.create_date();
6506                    let timestamp = dt.secs();
6507                    let datetime =
6508                        chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
6509                    datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
6510                };
6511
6512                crate::iam::GroupUser {
6513                    user_name: u.user_name().to_string(),
6514                    groups: String::new(),
6515                    last_activity: String::new(),
6516                    creation_time,
6517                }
6518            })
6519            .collect();
6520
6521        self.iam_state.group_users.items = group_users;
6522
6523        Ok(())
6524    }
6525
6526    pub async fn load_policy_document(
6527        &mut self,
6528        role_name: &str,
6529        policy_name: &str,
6530    ) -> anyhow::Result<()> {
6531        // Find the policy to get its ARN and type
6532        let policy = self
6533            .iam_state
6534            .policies
6535            .items
6536            .iter()
6537            .find(|p| p.policy_name == policy_name)
6538            .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
6539
6540        let document = if let Some(policy_arn) = &policy.policy_arn {
6541            // Managed policy - use get_policy_version
6542            self.iam_client
6543                .get_policy_version(policy_arn)
6544                .await
6545                .map_err(|e| anyhow::anyhow!(e))?
6546        } else {
6547            // Inline policy - use get_role_policy
6548            self.iam_client
6549                .get_role_policy(role_name, policy_name)
6550                .await
6551                .map_err(|e| anyhow::anyhow!(e))?
6552        };
6553
6554        self.iam_state.policy_document = document;
6555
6556        Ok(())
6557    }
6558
6559    pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
6560        let document = self
6561            .iam_client
6562            .get_role(role_name)
6563            .await
6564            .map_err(|e| anyhow::anyhow!(e))?;
6565
6566        self.iam_state.trust_policy_document = document;
6567
6568        Ok(())
6569    }
6570
6571    pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
6572        // TODO: Implement real AWS API call to get service last accessed details
6573        self.iam_state.last_accessed_services.items = vec![];
6574        self.iam_state.last_accessed_services.selected = 0;
6575
6576        Ok(())
6577    }
6578
6579    pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
6580        let tags = self
6581            .iam_client
6582            .list_role_tags(role_name)
6583            .await
6584            .map_err(|e| anyhow::anyhow!(e))?;
6585        self.iam_state.tags.items = tags
6586            .into_iter()
6587            .map(|(k, v)| crate::iam::RoleTag { key: k, value: v })
6588            .collect();
6589        self.iam_state.tags.reset();
6590        Ok(())
6591    }
6592
6593    pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
6594        let tags = self
6595            .iam_client
6596            .list_user_tags(user_name)
6597            .await
6598            .map_err(|e| anyhow::anyhow!(e))?;
6599        self.iam_state.user_tags.items = tags
6600            .into_iter()
6601            .map(|(k, v)| crate::iam::UserTag { key: k, value: v })
6602            .collect();
6603        self.iam_state.user_tags.reset();
6604        Ok(())
6605    }
6606
6607    pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
6608        if let Some(group) = self
6609            .log_groups_state
6610            .log_groups
6611            .items
6612            .get(self.log_groups_state.log_groups.selected)
6613        {
6614            self.log_groups_state.log_streams =
6615                self.cloudwatch_client.list_log_streams(&group.name).await?;
6616            self.log_groups_state.selected_stream = 0;
6617        }
6618        Ok(())
6619    }
6620
6621    pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
6622        if let Some(group) = self
6623            .log_groups_state
6624            .log_groups
6625            .items
6626            .get(self.log_groups_state.log_groups.selected)
6627        {
6628            if let Some(stream) = self
6629                .log_groups_state
6630                .log_streams
6631                .get(self.log_groups_state.selected_stream)
6632            {
6633                // Calculate time range from relative date picker
6634                let (start_time, end_time) =
6635                    if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
6636                        let now = chrono::Utc::now().timestamp_millis();
6637                        let duration_ms = match self.log_groups_state.relative_unit {
6638                            crate::app::TimeUnit::Minutes => amount * 60 * 1000,
6639                            crate::app::TimeUnit::Hours => amount * 60 * 60 * 1000,
6640                            crate::app::TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
6641                            crate::app::TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
6642                        };
6643                        (Some(now - duration_ms), Some(now))
6644                    } else {
6645                        (None, None)
6646                    };
6647
6648                let (mut events, has_more, token) = self
6649                    .cloudwatch_client
6650                    .get_log_events(
6651                        &group.name,
6652                        &stream.name,
6653                        self.log_groups_state.next_backward_token.clone(),
6654                        start_time,
6655                        end_time,
6656                    )
6657                    .await?;
6658
6659                if self.log_groups_state.next_backward_token.is_some() {
6660                    // Prepend older events - keep selection at top
6661                    events.append(&mut self.log_groups_state.log_events);
6662                    self.log_groups_state.event_scroll_offset = 0;
6663                } else {
6664                    // Initial load - start at first event
6665                    self.log_groups_state.event_scroll_offset = 0;
6666                }
6667
6668                self.log_groups_state.log_events = events;
6669                self.log_groups_state.has_older_events =
6670                    has_more && self.log_groups_state.log_events.len() >= 25;
6671                self.log_groups_state.next_backward_token = token;
6672                self.log_groups_state.selected_event = 0;
6673            }
6674        }
6675        Ok(())
6676    }
6677
6678    pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
6679        if self.insights_state.insights.selected_log_groups.is_empty() {
6680            return Err(anyhow::anyhow!(
6681                "No log groups selected. Please select at least one log group."
6682            ));
6683        }
6684
6685        let now = chrono::Utc::now().timestamp_millis();
6686        let amount = self
6687            .insights_state
6688            .insights
6689            .insights_relative_amount
6690            .parse::<i64>()
6691            .unwrap_or(1);
6692        let duration_ms = match self.insights_state.insights.insights_relative_unit {
6693            TimeUnit::Minutes => amount * 60 * 1000,
6694            TimeUnit::Hours => amount * 60 * 60 * 1000,
6695            TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
6696            TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
6697        };
6698        let start_time = now - duration_ms;
6699
6700        let query_id = self
6701            .cloudwatch_client
6702            .start_query(
6703                self.insights_state.insights.selected_log_groups.clone(),
6704                self.insights_state.insights.query_text.trim().to_string(),
6705                start_time,
6706                now,
6707            )
6708            .await?;
6709
6710        // Poll for results
6711        for _ in 0..60 {
6712            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
6713            let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
6714
6715            if status == "Complete" {
6716                self.insights_state.insights.query_results = results;
6717                self.insights_state.insights.query_completed = true;
6718                self.insights_state.insights.results_selected = 0;
6719                self.insights_state.insights.expanded_result = None;
6720                self.view_mode = ViewMode::InsightsResults;
6721                return Ok(());
6722            } else if status == "Failed" || status == "Cancelled" {
6723                return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
6724            }
6725        }
6726
6727        Err(anyhow::anyhow!("Query timeout"))
6728    }
6729}
6730
6731impl CloudWatchInsightsState {
6732    fn new() -> Self {
6733        Self {
6734            insights: InsightsState::default(),
6735            loading: false,
6736        }
6737    }
6738}
6739
6740impl CloudWatchAlarmsState {
6741    fn new() -> Self {
6742        Self {
6743            table: TableState::new(),
6744            alarm_tab: AlarmTab::AllAlarms,
6745            view_as: AlarmViewMode::Table,
6746            wrap_lines: false,
6747            sort_column: "Last state update".to_string(),
6748            sort_direction: SortDirection::Asc,
6749            input_focus: InputFocus::Filter,
6750        }
6751    }
6752}
6753
6754impl ServicePickerState {
6755    fn new() -> Self {
6756        Self {
6757            filter: String::new(),
6758            selected: 0,
6759            services: vec![
6760                "CloudWatch > Log Groups",
6761                "CloudWatch > Logs Insights",
6762                "CloudWatch > Alarms",
6763                "CloudFormation > Stacks",
6764                "ECR > Repositories",
6765                "IAM > Users",
6766                "IAM > Roles",
6767                "IAM > User Groups",
6768                "Lambda > Functions",
6769                "Lambda > Applications",
6770                "S3 > Buckets",
6771                "SQS > Queues",
6772            ],
6773        }
6774    }
6775}
6776
6777#[cfg(test)]
6778mod test_helpers {
6779    use super::*;
6780
6781    // Test helper functions to reduce boilerplate
6782    #[allow(dead_code)]
6783    pub fn test_app() -> App {
6784        App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
6785    }
6786
6787    #[allow(dead_code)]
6788    pub fn test_app_no_region() -> App {
6789        App::new_without_client("test".to_string(), None)
6790    }
6791
6792    #[allow(dead_code)]
6793    pub fn test_tab(service: Service) -> Tab {
6794        Tab {
6795            service,
6796            title: service.name().to_string(),
6797            breadcrumb: service.name().to_string(),
6798        }
6799    }
6800
6801    #[allow(dead_code)]
6802    pub fn test_iam_role(name: &str) -> crate::iam::IamRole {
6803        crate::iam::IamRole {
6804            role_name: name.to_string(),
6805            path: "/".to_string(),
6806            description: format!("Test role {}", name),
6807            trusted_entities: "AWS Service: ec2.amazonaws.com".to_string(),
6808            creation_time: "2024-01-01 00:00:00".to_string(),
6809            arn: format!("arn:aws:iam::123456789012:role/{}", name),
6810            max_session_duration: Some(3600),
6811            last_activity: "-".to_string(),
6812        }
6813    }
6814}
6815
6816#[cfg(test)]
6817mod tests {
6818    use super::*;
6819    use crate::keymap::Action;
6820    use test_helpers::*;
6821
6822    #[test]
6823    fn test_next_tab_cycles_forward() {
6824        let mut app = test_app();
6825        app.tabs = vec![
6826            Tab {
6827                service: Service::CloudWatchLogGroups,
6828                title: "CloudWatch > Log Groups".to_string(),
6829                breadcrumb: "CloudWatch > Log Groups".to_string(),
6830            },
6831            Tab {
6832                service: Service::CloudWatchInsights,
6833                title: "CloudWatch > Logs Insights".to_string(),
6834                breadcrumb: "CloudWatch > Logs Insights".to_string(),
6835            },
6836            Tab {
6837                service: Service::CloudWatchAlarms,
6838                title: "CloudWatch > Alarms".to_string(),
6839                breadcrumb: "CloudWatch > Alarms".to_string(),
6840            },
6841        ];
6842        app.current_tab = 0;
6843
6844        app.handle_action(Action::NextTab);
6845        assert_eq!(app.current_tab, 1);
6846        assert_eq!(app.current_service, Service::CloudWatchInsights);
6847
6848        app.handle_action(Action::NextTab);
6849        assert_eq!(app.current_tab, 2);
6850        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6851
6852        // Should wrap around
6853        app.handle_action(Action::NextTab);
6854        assert_eq!(app.current_tab, 0);
6855        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
6856    }
6857
6858    #[test]
6859    fn test_prev_tab_cycles_backward() {
6860        let mut app = test_app();
6861        app.tabs = vec![
6862            Tab {
6863                service: Service::CloudWatchLogGroups,
6864                title: "CloudWatch > Log Groups".to_string(),
6865                breadcrumb: "CloudWatch > Log Groups".to_string(),
6866            },
6867            Tab {
6868                service: Service::CloudWatchInsights,
6869                title: "CloudWatch > Logs Insights".to_string(),
6870                breadcrumb: "CloudWatch > Logs Insights".to_string(),
6871            },
6872            Tab {
6873                service: Service::CloudWatchAlarms,
6874                title: "CloudWatch > Alarms".to_string(),
6875                breadcrumb: "CloudWatch > Alarms".to_string(),
6876            },
6877        ];
6878        app.current_tab = 2;
6879
6880        app.handle_action(Action::PrevTab);
6881        assert_eq!(app.current_tab, 1);
6882        assert_eq!(app.current_service, Service::CloudWatchInsights);
6883
6884        app.handle_action(Action::PrevTab);
6885        assert_eq!(app.current_tab, 0);
6886        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
6887
6888        // Should wrap around
6889        app.handle_action(Action::PrevTab);
6890        assert_eq!(app.current_tab, 2);
6891        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6892    }
6893
6894    #[test]
6895    fn test_close_tab_removes_current() {
6896        let mut app = test_app();
6897        app.tabs = vec![
6898            Tab {
6899                service: Service::CloudWatchLogGroups,
6900                title: "CloudWatch > Log Groups".to_string(),
6901                breadcrumb: "CloudWatch > Log Groups".to_string(),
6902            },
6903            Tab {
6904                service: Service::CloudWatchInsights,
6905                title: "CloudWatch > Logs Insights".to_string(),
6906                breadcrumb: "CloudWatch > Logs Insights".to_string(),
6907            },
6908            Tab {
6909                service: Service::CloudWatchAlarms,
6910                title: "CloudWatch > Alarms".to_string(),
6911                breadcrumb: "CloudWatch > Alarms".to_string(),
6912            },
6913        ];
6914        app.current_tab = 1;
6915        app.service_selected = true;
6916
6917        app.handle_action(Action::CloseTab);
6918        assert_eq!(app.tabs.len(), 2);
6919        assert_eq!(app.current_tab, 1);
6920        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6921    }
6922
6923    #[test]
6924    fn test_close_last_tab_exits_service() {
6925        let mut app = test_app();
6926        app.tabs = vec![Tab {
6927            service: Service::CloudWatchLogGroups,
6928            title: "CloudWatch > Log Groups".to_string(),
6929            breadcrumb: "CloudWatch > Log Groups".to_string(),
6930        }];
6931        app.current_tab = 0;
6932        app.service_selected = true;
6933
6934        app.handle_action(Action::CloseTab);
6935        assert_eq!(app.tabs.len(), 0);
6936        assert!(!app.service_selected);
6937        assert_eq!(app.current_tab, 0);
6938    }
6939
6940    #[test]
6941    fn test_close_service_removes_current_tab() {
6942        let mut app = test_app();
6943        app.tabs = vec![
6944            Tab {
6945                service: Service::CloudWatchLogGroups,
6946                title: "CloudWatch > Log Groups".to_string(),
6947                breadcrumb: "CloudWatch > Log Groups".to_string(),
6948            },
6949            Tab {
6950                service: Service::CloudWatchInsights,
6951                title: "CloudWatch > Logs Insights".to_string(),
6952                breadcrumb: "CloudWatch > Logs Insights".to_string(),
6953            },
6954            Tab {
6955                service: Service::CloudWatchAlarms,
6956                title: "CloudWatch > Alarms".to_string(),
6957                breadcrumb: "CloudWatch > Alarms".to_string(),
6958            },
6959        ];
6960        app.current_tab = 1;
6961        app.service_selected = true;
6962
6963        app.handle_action(Action::CloseService);
6964
6965        // Tab should be removed
6966        assert_eq!(app.tabs.len(), 2);
6967        // Should switch to next tab (Alarms at index 1)
6968        assert_eq!(app.current_tab, 1);
6969        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6970        // Should stay in service mode, NOT show service picker
6971        assert!(app.service_selected);
6972        assert_eq!(app.mode, Mode::Normal);
6973    }
6974
6975    #[test]
6976    fn test_close_service_last_tab_shows_picker() {
6977        let mut app = test_app();
6978        app.tabs = vec![Tab {
6979            service: Service::CloudWatchLogGroups,
6980            title: "CloudWatch > Log Groups".to_string(),
6981            breadcrumb: "CloudWatch > Log Groups".to_string(),
6982        }];
6983        app.current_tab = 0;
6984        app.service_selected = true;
6985
6986        app.handle_action(Action::CloseService);
6987
6988        // Tab should be removed
6989        assert_eq!(app.tabs.len(), 0);
6990        // Should show service picker
6991        assert!(!app.service_selected);
6992        assert_eq!(app.mode, Mode::ServicePicker);
6993    }
6994
6995    #[test]
6996    fn test_open_tab_picker_with_tabs() {
6997        let mut app = test_app();
6998        app.tabs = vec![
6999            Tab {
7000                service: Service::CloudWatchLogGroups,
7001                title: "CloudWatch > Log Groups".to_string(),
7002                breadcrumb: "CloudWatch > Log Groups".to_string(),
7003            },
7004            Tab {
7005                service: Service::CloudWatchInsights,
7006                title: "CloudWatch > Logs Insights".to_string(),
7007                breadcrumb: "CloudWatch > Logs Insights".to_string(),
7008            },
7009        ];
7010        app.current_tab = 1;
7011
7012        app.handle_action(Action::OpenTabPicker);
7013        assert_eq!(app.mode, Mode::TabPicker);
7014        assert_eq!(app.tab_picker_selected, 1);
7015    }
7016
7017    #[test]
7018    fn test_open_tab_picker_without_tabs() {
7019        let mut app = test_app();
7020        app.tabs = vec![];
7021
7022        app.handle_action(Action::OpenTabPicker);
7023        assert_eq!(app.mode, Mode::Normal);
7024    }
7025
7026    #[test]
7027    fn test_pending_key_state() {
7028        let mut app = test_app();
7029        assert_eq!(app.pending_key, None);
7030
7031        app.pending_key = Some('g');
7032        assert_eq!(app.pending_key, Some('g'));
7033    }
7034
7035    #[test]
7036    fn test_tab_breadcrumb_updates() {
7037        let mut app = test_app();
7038        app.tabs = vec![Tab {
7039            service: Service::CloudWatchLogGroups,
7040            title: "CloudWatch > Log Groups".to_string(),
7041            breadcrumb: "CloudWatch > Log groups".to_string(),
7042        }];
7043        app.current_tab = 0;
7044        app.service_selected = true;
7045        app.current_service = Service::CloudWatchLogGroups;
7046
7047        // Initial breadcrumb
7048        assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
7049
7050        // Add a log group and update breadcrumb
7051        app.log_groups_state
7052            .log_groups
7053            .items
7054            .push(rusticity_core::LogGroup {
7055                name: "/aws/lambda/test".to_string(),
7056                creation_time: None,
7057                stored_bytes: Some(1024),
7058                retention_days: None,
7059                log_class: None,
7060                arn: None,
7061            });
7062        app.log_groups_state.log_groups.reset();
7063        app.view_mode = ViewMode::Detail;
7064        app.update_current_tab_breadcrumb();
7065
7066        // Breadcrumb should now include log group
7067        assert_eq!(
7068            app.tabs[0].breadcrumb,
7069            "CloudWatch > Log groups > /aws/lambda/test"
7070        );
7071    }
7072
7073    #[test]
7074    fn test_s3_bucket_column_selector_navigation() {
7075        let mut app = test_app();
7076        app.current_service = Service::S3Buckets;
7077        app.mode = Mode::ColumnSelector;
7078        app.column_selector_index = 0;
7079
7080        // Should navigate through 3 S3 bucket columns (0, 1, 2)
7081        app.handle_action(Action::NextItem);
7082        assert_eq!(app.column_selector_index, 1);
7083
7084        app.handle_action(Action::NextItem);
7085        assert_eq!(app.column_selector_index, 2);
7086
7087        // Should not go beyond max (2)
7088        app.handle_action(Action::NextItem);
7089        assert_eq!(app.column_selector_index, 2);
7090
7091        // Navigate back
7092        app.handle_action(Action::PrevItem);
7093        assert_eq!(app.column_selector_index, 1);
7094
7095        app.handle_action(Action::PrevItem);
7096        assert_eq!(app.column_selector_index, 0);
7097
7098        // Should not go below 0
7099        app.handle_action(Action::PrevItem);
7100        assert_eq!(app.column_selector_index, 0);
7101    }
7102
7103    #[test]
7104    fn test_cloudwatch_alarms_state_initialized() {
7105        let app = test_app();
7106
7107        // Alarms state should be initialized
7108        assert_eq!(app.alarms_state.table.items.len(), 0);
7109        assert_eq!(app.alarms_state.table.selected, 0);
7110        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
7111        assert!(!app.alarms_state.table.loading);
7112        assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
7113        assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
7114    }
7115
7116    #[test]
7117    fn test_cloudwatch_alarms_service_selection() {
7118        let mut app = test_app();
7119
7120        // Switch to alarms service
7121        app.current_service = Service::CloudWatchAlarms;
7122        app.service_selected = true;
7123
7124        assert_eq!(app.current_service, Service::CloudWatchAlarms);
7125        assert!(app.service_selected);
7126    }
7127
7128    #[test]
7129    fn test_cloudwatch_alarms_column_preferences() {
7130        let app = test_app();
7131
7132        // Should have alarm columns defined
7133        assert!(!app.cw_alarm_column_ids.is_empty());
7134        assert!(!app.cw_alarm_visible_column_ids.is_empty());
7135
7136        // Default visible columns
7137        assert!(app
7138            .cw_alarm_visible_column_ids
7139            .contains(&AlarmColumn::Name.id()));
7140        assert!(app
7141            .cw_alarm_visible_column_ids
7142            .contains(&AlarmColumn::State.id()));
7143    }
7144
7145    #[test]
7146    fn test_s3_bucket_navigation_without_expansion() {
7147        let mut app = test_app();
7148        app.current_service = Service::S3Buckets;
7149        app.service_selected = true;
7150        app.mode = Mode::Normal;
7151
7152        // Add 3 buckets
7153        app.s3_state.buckets.items = vec![
7154            S3Bucket {
7155                name: "bucket1".to_string(),
7156                region: "us-east-1".to_string(),
7157                creation_date: "2024-01-01T00:00:00Z".to_string(),
7158            },
7159            S3Bucket {
7160                name: "bucket2".to_string(),
7161                region: "us-east-1".to_string(),
7162                creation_date: "2024-01-02T00:00:00Z".to_string(),
7163            },
7164            S3Bucket {
7165                name: "bucket3".to_string(),
7166                region: "us-east-1".to_string(),
7167                creation_date: "2024-01-03T00:00:00Z".to_string(),
7168            },
7169        ];
7170        app.s3_state.selected_row = 0;
7171
7172        // Navigate down
7173        app.handle_action(Action::NextItem);
7174        assert_eq!(app.s3_state.selected_row, 1);
7175
7176        app.handle_action(Action::NextItem);
7177        assert_eq!(app.s3_state.selected_row, 2);
7178
7179        // Should not go beyond last bucket
7180        app.handle_action(Action::NextItem);
7181        assert_eq!(app.s3_state.selected_row, 2);
7182
7183        // Navigate up
7184        app.handle_action(Action::PrevItem);
7185        assert_eq!(app.s3_state.selected_row, 1);
7186
7187        app.handle_action(Action::PrevItem);
7188        assert_eq!(app.s3_state.selected_row, 0);
7189
7190        // Should not go below 0
7191        app.handle_action(Action::PrevItem);
7192        assert_eq!(app.s3_state.selected_row, 0);
7193    }
7194
7195    #[test]
7196    fn test_s3_bucket_navigation_with_expansion() {
7197        let mut app = test_app();
7198        app.current_service = Service::S3Buckets;
7199        app.service_selected = true;
7200        app.mode = Mode::Normal;
7201
7202        // Add 2 buckets
7203        app.s3_state.buckets.items = vec![
7204            S3Bucket {
7205                name: "bucket1".to_string(),
7206                region: "us-east-1".to_string(),
7207                creation_date: "2024-01-01T00:00:00Z".to_string(),
7208            },
7209            S3Bucket {
7210                name: "bucket2".to_string(),
7211                region: "us-east-1".to_string(),
7212                creation_date: "2024-01-02T00:00:00Z".to_string(),
7213            },
7214        ];
7215
7216        // Expand bucket1 with 2 objects
7217        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
7218        app.s3_state.bucket_preview.insert(
7219            "bucket1".to_string(),
7220            vec![
7221                S3Object {
7222                    key: "file1.txt".to_string(),
7223                    size: 100,
7224                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7225                    is_prefix: false,
7226                    storage_class: "STANDARD".to_string(),
7227                },
7228                S3Object {
7229                    key: "folder/".to_string(),
7230                    size: 0,
7231                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7232                    is_prefix: true,
7233                    storage_class: String::new(),
7234                },
7235            ],
7236        );
7237
7238        app.s3_state.selected_row = 0;
7239
7240        // Total rows: bucket1 (row 0) + file1.txt (row 1) + folder/ (row 2) + bucket2 (row 3) = 4 rows
7241        // Navigate through all rows
7242        app.handle_action(Action::NextItem);
7243        assert_eq!(app.s3_state.selected_row, 1); // file1.txt
7244
7245        app.handle_action(Action::NextItem);
7246        assert_eq!(app.s3_state.selected_row, 2); // folder/
7247
7248        app.handle_action(Action::NextItem);
7249        assert_eq!(app.s3_state.selected_row, 3); // bucket2
7250
7251        // Should not go beyond last row
7252        app.handle_action(Action::NextItem);
7253        assert_eq!(app.s3_state.selected_row, 3);
7254    }
7255
7256    #[test]
7257    fn test_s3_bucket_navigation_with_nested_expansion() {
7258        let mut app = test_app();
7259        app.current_service = Service::S3Buckets;
7260        app.service_selected = true;
7261        app.mode = Mode::Normal;
7262
7263        // Add 1 bucket
7264        app.s3_state.buckets.items = vec![S3Bucket {
7265            name: "bucket1".to_string(),
7266            region: "us-east-1".to_string(),
7267            creation_date: "2024-01-01T00:00:00Z".to_string(),
7268        }];
7269
7270        // Expand bucket1 with a folder
7271        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
7272        app.s3_state.bucket_preview.insert(
7273            "bucket1".to_string(),
7274            vec![S3Object {
7275                key: "folder/".to_string(),
7276                size: 0,
7277                last_modified: "2024-01-01T00:00:00Z".to_string(),
7278                is_prefix: true,
7279                storage_class: String::new(),
7280            }],
7281        );
7282
7283        // Expand the folder with 2 nested objects
7284        app.s3_state.expanded_prefixes.insert("folder/".to_string());
7285        app.s3_state.prefix_preview.insert(
7286            "folder/".to_string(),
7287            vec![
7288                S3Object {
7289                    key: "folder/file1.txt".to_string(),
7290                    size: 100,
7291                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7292                    is_prefix: false,
7293                    storage_class: "STANDARD".to_string(),
7294                },
7295                S3Object {
7296                    key: "folder/file2.txt".to_string(),
7297                    size: 200,
7298                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7299                    is_prefix: false,
7300                    storage_class: "STANDARD".to_string(),
7301                },
7302            ],
7303        );
7304
7305        app.s3_state.selected_row = 0;
7306
7307        // Total rows: bucket1 (0) + folder/ (1) + file1.txt (2) + file2.txt (3) = 4 rows
7308        app.handle_action(Action::NextItem);
7309        assert_eq!(app.s3_state.selected_row, 1); // folder/
7310
7311        app.handle_action(Action::NextItem);
7312        assert_eq!(app.s3_state.selected_row, 2); // folder/file1.txt
7313
7314        app.handle_action(Action::NextItem);
7315        assert_eq!(app.s3_state.selected_row, 3); // folder/file2.txt
7316
7317        // Should not go beyond last row
7318        app.handle_action(Action::NextItem);
7319        assert_eq!(app.s3_state.selected_row, 3);
7320    }
7321
7322    #[test]
7323    fn test_calculate_total_bucket_rows() {
7324        let mut app = test_app();
7325
7326        // No buckets
7327        assert_eq!(app.calculate_total_bucket_rows(), 0);
7328
7329        // 2 buckets, no expansion
7330        app.s3_state.buckets.items = vec![
7331            S3Bucket {
7332                name: "bucket1".to_string(),
7333                region: "us-east-1".to_string(),
7334                creation_date: "2024-01-01T00:00:00Z".to_string(),
7335            },
7336            S3Bucket {
7337                name: "bucket2".to_string(),
7338                region: "us-east-1".to_string(),
7339                creation_date: "2024-01-02T00:00:00Z".to_string(),
7340            },
7341        ];
7342        assert_eq!(app.calculate_total_bucket_rows(), 2);
7343
7344        // Expand bucket1 with 3 objects
7345        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
7346        app.s3_state.bucket_preview.insert(
7347            "bucket1".to_string(),
7348            vec![
7349                S3Object {
7350                    key: "file1.txt".to_string(),
7351                    size: 100,
7352                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7353                    is_prefix: false,
7354                    storage_class: "STANDARD".to_string(),
7355                },
7356                S3Object {
7357                    key: "file2.txt".to_string(),
7358                    size: 200,
7359                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7360                    is_prefix: false,
7361                    storage_class: "STANDARD".to_string(),
7362                },
7363                S3Object {
7364                    key: "folder/".to_string(),
7365                    size: 0,
7366                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7367                    is_prefix: true,
7368                    storage_class: String::new(),
7369                },
7370            ],
7371        );
7372        assert_eq!(app.calculate_total_bucket_rows(), 5); // 2 buckets + 3 objects
7373
7374        // Expand folder/ with 2 nested objects
7375        app.s3_state.expanded_prefixes.insert("folder/".to_string());
7376        app.s3_state.prefix_preview.insert(
7377            "folder/".to_string(),
7378            vec![
7379                S3Object {
7380                    key: "folder/nested1.txt".to_string(),
7381                    size: 50,
7382                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7383                    is_prefix: false,
7384                    storage_class: "STANDARD".to_string(),
7385                },
7386                S3Object {
7387                    key: "folder/nested2.txt".to_string(),
7388                    size: 75,
7389                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7390                    is_prefix: false,
7391                    storage_class: "STANDARD".to_string(),
7392                },
7393            ],
7394        );
7395        assert_eq!(app.calculate_total_bucket_rows(), 7); // 2 buckets + 3 objects + 2 nested
7396    }
7397
7398    #[test]
7399    fn test_calculate_total_object_rows() {
7400        let mut app = test_app();
7401        app.s3_state.current_bucket = Some("test-bucket".to_string());
7402
7403        // No objects
7404        assert_eq!(app.calculate_total_object_rows(), 0);
7405
7406        // 2 objects, no expansion
7407        app.s3_state.objects = vec![
7408            S3Object {
7409                key: "file1.txt".to_string(),
7410                size: 100,
7411                last_modified: "2024-01-01T00:00:00Z".to_string(),
7412                is_prefix: false,
7413                storage_class: "STANDARD".to_string(),
7414            },
7415            S3Object {
7416                key: "folder/".to_string(),
7417                size: 0,
7418                last_modified: "2024-01-01T00:00:00Z".to_string(),
7419                is_prefix: true,
7420                storage_class: String::new(),
7421            },
7422        ];
7423        assert_eq!(app.calculate_total_object_rows(), 2);
7424
7425        // Expand folder/ with 2 items
7426        app.s3_state.expanded_prefixes.insert("folder/".to_string());
7427        app.s3_state.prefix_preview.insert(
7428            "folder/".to_string(),
7429            vec![
7430                S3Object {
7431                    key: "folder/file2.txt".to_string(),
7432                    size: 200,
7433                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7434                    is_prefix: false,
7435                    storage_class: "STANDARD".to_string(),
7436                },
7437                S3Object {
7438                    key: "folder/subfolder/".to_string(),
7439                    size: 0,
7440                    last_modified: "2024-01-01T00:00:00Z".to_string(),
7441                    is_prefix: true,
7442                    storage_class: String::new(),
7443                },
7444            ],
7445        );
7446        assert_eq!(app.calculate_total_object_rows(), 4); // 2 + 2 nested
7447
7448        // Expand subfolder/ with 1 item (3rd level)
7449        app.s3_state
7450            .expanded_prefixes
7451            .insert("folder/subfolder/".to_string());
7452        app.s3_state.prefix_preview.insert(
7453            "folder/subfolder/".to_string(),
7454            vec![S3Object {
7455                key: "folder/subfolder/deep.txt".to_string(),
7456                size: 50,
7457                last_modified: "2024-01-01T00:00:00Z".to_string(),
7458                is_prefix: false,
7459                storage_class: "STANDARD".to_string(),
7460            }],
7461        );
7462        assert_eq!(app.calculate_total_object_rows(), 5); // 2 + 2 nested + 1 deep
7463    }
7464
7465    #[test]
7466    fn test_s3_object_navigation_with_deep_nesting() {
7467        let mut app = test_app();
7468        app.current_service = Service::S3Buckets;
7469        app.service_selected = true;
7470        app.mode = Mode::Normal;
7471        app.s3_state.current_bucket = Some("test-bucket".to_string());
7472
7473        // Add folder structure: folder1/ -> folder2/ -> file.txt
7474        app.s3_state.objects = vec![S3Object {
7475            key: "folder1/".to_string(),
7476            size: 0,
7477            last_modified: "2024-01-01T00:00:00Z".to_string(),
7478            is_prefix: true,
7479            storage_class: String::new(),
7480        }];
7481
7482        // Expand folder1/
7483        app.s3_state
7484            .expanded_prefixes
7485            .insert("folder1/".to_string());
7486        app.s3_state.prefix_preview.insert(
7487            "folder1/".to_string(),
7488            vec![S3Object {
7489                key: "folder1/folder2/".to_string(),
7490                size: 0,
7491                last_modified: "2024-01-01T00:00:00Z".to_string(),
7492                is_prefix: true,
7493                storage_class: String::new(),
7494            }],
7495        );
7496
7497        // Expand folder2/
7498        app.s3_state
7499            .expanded_prefixes
7500            .insert("folder1/folder2/".to_string());
7501        app.s3_state.prefix_preview.insert(
7502            "folder1/folder2/".to_string(),
7503            vec![S3Object {
7504                key: "folder1/folder2/file.txt".to_string(),
7505                size: 100,
7506                last_modified: "2024-01-01T00:00:00Z".to_string(),
7507                is_prefix: false,
7508                storage_class: "STANDARD".to_string(),
7509            }],
7510        );
7511
7512        app.s3_state.selected_object = 0;
7513
7514        // Total: folder1/ (0) + folder2/ (1) + file.txt (2) = 3 rows
7515        app.handle_action(Action::NextItem);
7516        assert_eq!(app.s3_state.selected_object, 1); // folder2/
7517
7518        app.handle_action(Action::NextItem);
7519        assert_eq!(app.s3_state.selected_object, 2); // file.txt
7520
7521        // Should not go beyond
7522        app.handle_action(Action::NextItem);
7523        assert_eq!(app.s3_state.selected_object, 2);
7524    }
7525
7526    #[test]
7527    fn test_s3_expand_nested_folder_in_objects_view() {
7528        let mut app = test_app();
7529        app.current_service = Service::S3Buckets;
7530        app.service_selected = true;
7531        app.mode = Mode::Normal;
7532        app.s3_state.current_bucket = Some("test-bucket".to_string());
7533
7534        // Add parent folder
7535        app.s3_state.objects = vec![S3Object {
7536            key: "parent/".to_string(),
7537            size: 0,
7538            last_modified: "2024-01-01T00:00:00Z".to_string(),
7539            is_prefix: true,
7540            storage_class: String::new(),
7541        }];
7542
7543        // Expand parent
7544        app.s3_state.expanded_prefixes.insert("parent/".to_string());
7545        app.s3_state.prefix_preview.insert(
7546            "parent/".to_string(),
7547            vec![S3Object {
7548                key: "parent/child/".to_string(),
7549                size: 0,
7550                last_modified: "2024-01-01T00:00:00Z".to_string(),
7551                is_prefix: true,
7552                storage_class: String::new(),
7553            }],
7554        );
7555
7556        // Select the nested folder (index 1)
7557        app.s3_state.selected_object = 1;
7558
7559        // Expand it (simulate pressing Enter/Right)
7560        app.handle_action(Action::NextPane);
7561
7562        // Should be expanded now
7563        assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
7564        assert!(app.s3_state.buckets.loading); // Should trigger load
7565    }
7566
7567    #[test]
7568    fn test_s3_drill_into_nested_folder() {
7569        let mut app = test_app();
7570        app.current_service = Service::S3Buckets;
7571        app.service_selected = true;
7572        app.mode = Mode::Normal;
7573        app.s3_state.current_bucket = Some("test-bucket".to_string());
7574
7575        // Add parent folder
7576        app.s3_state.objects = vec![S3Object {
7577            key: "parent/".to_string(),
7578            size: 0,
7579            last_modified: "2024-01-01T00:00:00Z".to_string(),
7580            is_prefix: true,
7581            storage_class: String::new(),
7582        }];
7583
7584        // Expand parent
7585        app.s3_state.expanded_prefixes.insert("parent/".to_string());
7586        app.s3_state.prefix_preview.insert(
7587            "parent/".to_string(),
7588            vec![S3Object {
7589                key: "parent/child/".to_string(),
7590                size: 0,
7591                last_modified: "2024-01-01T00:00:00Z".to_string(),
7592                is_prefix: true,
7593                storage_class: String::new(),
7594            }],
7595        );
7596
7597        // Select the nested folder (index 1)
7598        app.s3_state.selected_object = 1;
7599
7600        // Drill into it (simulate pressing Enter)
7601        app.handle_action(Action::Select);
7602
7603        // Should navigate into the folder
7604        assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
7605        assert!(app.s3_state.buckets.loading); // Should trigger load
7606    }
7607
7608    #[test]
7609    fn test_s3_esc_pops_navigation_stack() {
7610        let mut app = test_app();
7611        app.current_service = Service::S3Buckets;
7612        app.s3_state.current_bucket = Some("test-bucket".to_string());
7613        app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
7614
7615        // Press Esc - should pop from stack
7616        app.handle_action(Action::GoBack);
7617        assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
7618        assert!(app.s3_state.buckets.loading);
7619
7620        // Press Esc again - should pop to bucket root
7621        app.s3_state.buckets.loading = false;
7622        app.handle_action(Action::GoBack);
7623        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
7624        assert!(app.s3_state.buckets.loading);
7625
7626        // Press Esc again - should exit bucket
7627        app.s3_state.buckets.loading = false;
7628        app.handle_action(Action::GoBack);
7629        assert_eq!(app.s3_state.current_bucket, None);
7630    }
7631
7632    #[test]
7633    fn test_s3_esc_from_bucket_root_exits() {
7634        let mut app = test_app();
7635        app.current_service = Service::S3Buckets;
7636        app.s3_state.current_bucket = Some("test-bucket".to_string());
7637        app.s3_state.prefix_stack = vec![];
7638
7639        // Press Esc from bucket root - should exit bucket
7640        app.handle_action(Action::GoBack);
7641        assert_eq!(app.s3_state.current_bucket, None);
7642        assert_eq!(app.s3_state.objects.len(), 0);
7643    }
7644
7645    #[test]
7646    fn test_s3_drill_into_nested_prefix_from_bucket_list() {
7647        let mut app = test_app();
7648        app.current_service = Service::S3Buckets;
7649        app.service_selected = true;
7650        app.mode = Mode::Normal;
7651
7652        // Setup bucket with nested preview
7653        app.s3_state.buckets.items = vec![S3Bucket {
7654            name: "test-bucket".to_string(),
7655            region: "us-east-1".to_string(),
7656            creation_date: "2024-01-01".to_string(),
7657        }];
7658
7659        // Expand bucket to show first-level prefix
7660        app.s3_state
7661            .expanded_prefixes
7662            .insert("test-bucket".to_string());
7663        app.s3_state.bucket_preview.insert(
7664            "test-bucket".to_string(),
7665            vec![S3Object {
7666                key: "parent/".to_string(),
7667                size: 0,
7668                last_modified: "2024-01-01".to_string(),
7669                is_prefix: true,
7670                storage_class: String::new(),
7671            }],
7672        );
7673
7674        // Expand parent to show nested prefix
7675        app.s3_state.expanded_prefixes.insert("parent/".to_string());
7676        app.s3_state.prefix_preview.insert(
7677            "parent/".to_string(),
7678            vec![S3Object {
7679                key: "parent/child/".to_string(),
7680                size: 0,
7681                last_modified: "2024-01-01".to_string(),
7682                is_prefix: true,
7683                storage_class: String::new(),
7684            }],
7685        );
7686
7687        // Select nested prefix (row 2: bucket, parent, nested)
7688        app.s3_state.selected_row = 2;
7689
7690        // Drill into nested prefix
7691        app.handle_action(Action::Select);
7692
7693        // Should have both parent and child in stack
7694        assert_eq!(
7695            app.s3_state.prefix_stack,
7696            vec!["parent/".to_string(), "parent/child/".to_string()]
7697        );
7698        assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
7699        assert!(app.s3_state.buckets.loading);
7700
7701        // Now press Esc - should go back to parent
7702        app.s3_state.buckets.loading = false;
7703        app.handle_action(Action::GoBack);
7704        assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
7705        assert!(app.s3_state.buckets.loading);
7706
7707        // Press Esc again - should go to bucket root
7708        app.s3_state.buckets.loading = false;
7709        app.handle_action(Action::GoBack);
7710        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
7711        assert!(app.s3_state.buckets.loading);
7712
7713        // Press Esc again - should exit bucket
7714        app.s3_state.buckets.loading = false;
7715        app.handle_action(Action::GoBack);
7716        assert_eq!(app.s3_state.current_bucket, None);
7717    }
7718
7719    #[test]
7720    fn test_region_picker_fuzzy_filter() {
7721        let mut app = test_app();
7722        app.region_latencies.insert("us-east-1".to_string(), 10);
7723        app.region_filter = "vir".to_string();
7724        let filtered = app.get_filtered_regions();
7725        assert!(filtered.iter().any(|r| r.code == "us-east-1"));
7726    }
7727
7728    #[test]
7729    fn test_profile_picker_loads_profiles() {
7730        let profiles = App::load_aws_profiles();
7731        // Should at least have default profile or be empty if no config
7732        assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
7733    }
7734
7735    #[test]
7736    fn test_profile_with_region_uses_it() {
7737        let mut app = test_app_no_region();
7738        app.available_profiles = vec![AwsProfile {
7739            name: "test-profile".to_string(),
7740            region: Some("eu-west-1".to_string()),
7741            account: Some("123456789".to_string()),
7742            role_arn: None,
7743            source_profile: None,
7744        }];
7745        app.profile_picker_selected = 0;
7746        app.mode = Mode::ProfilePicker;
7747
7748        // Simulate selecting the profile
7749        let filtered = app.get_filtered_profiles();
7750        if let Some(profile) = filtered.first() {
7751            let profile_name = profile.name.clone();
7752            let profile_region = profile.region.clone();
7753
7754            app.profile = profile_name;
7755            if let Some(region) = profile_region {
7756                app.region = region;
7757            }
7758        }
7759
7760        assert_eq!(app.profile, "test-profile");
7761        assert_eq!(app.region, "eu-west-1");
7762    }
7763
7764    #[test]
7765    fn test_profile_without_region_keeps_unknown() {
7766        let mut app = test_app_no_region();
7767        let initial_region = app.region.clone();
7768
7769        app.available_profiles = vec![AwsProfile {
7770            name: "test-profile".to_string(),
7771            region: None,
7772            account: None,
7773            role_arn: None,
7774            source_profile: None,
7775        }];
7776        app.profile_picker_selected = 0;
7777        app.mode = Mode::ProfilePicker;
7778
7779        let filtered = app.get_filtered_profiles();
7780        if let Some(profile) = filtered.first() {
7781            let profile_name = profile.name.clone();
7782            let profile_region = profile.region.clone();
7783
7784            app.profile = profile_name;
7785            if let Some(region) = profile_region {
7786                app.region = region;
7787            }
7788        }
7789
7790        assert_eq!(app.profile, "test-profile");
7791        assert_eq!(app.region, initial_region); // Should keep initial region
7792    }
7793
7794    #[test]
7795    fn test_region_selection_closes_all_tabs() {
7796        let mut app = test_app();
7797
7798        // Add some tabs
7799        app.tabs.push(Tab {
7800            service: Service::CloudWatchLogGroups,
7801            title: "CloudWatch".to_string(),
7802            breadcrumb: "CloudWatch".to_string(),
7803        });
7804        app.tabs.push(Tab {
7805            service: Service::S3Buckets,
7806            title: "S3".to_string(),
7807            breadcrumb: "S3".to_string(),
7808        });
7809        app.service_selected = true;
7810        app.current_tab = 1;
7811
7812        // Add latency for region
7813        app.region_latencies.insert("eu-west-1".to_string(), 50);
7814
7815        // Simulate selecting a different region
7816        app.mode = Mode::RegionPicker;
7817        app.region_picker_selected = 0;
7818
7819        let filtered = app.get_filtered_regions();
7820        if let Some(region) = filtered.first() {
7821            app.region = region.code.to_string();
7822            app.tabs.clear();
7823            app.current_tab = 0;
7824            app.service_selected = false;
7825            app.mode = Mode::Normal;
7826        }
7827
7828        assert_eq!(app.tabs.len(), 0);
7829        assert_eq!(app.current_tab, 0);
7830        assert!(!app.service_selected);
7831        assert_eq!(app.region, "eu-west-1");
7832    }
7833
7834    #[test]
7835    fn test_region_picker_can_be_closed_without_selection() {
7836        let mut app = test_app();
7837        let initial_region = app.region.clone();
7838
7839        app.mode = Mode::RegionPicker;
7840
7841        // Close without selecting (Esc)
7842        app.mode = Mode::Normal;
7843
7844        // Region should not change
7845        assert_eq!(app.region, initial_region);
7846    }
7847
7848    #[test]
7849    fn test_session_filter_works() {
7850        let mut app = test_app();
7851
7852        app.sessions = vec![
7853            Session {
7854                id: "1".to_string(),
7855                timestamp: "2024-01-01".to_string(),
7856                profile: "prod-profile".to_string(),
7857                region: "us-east-1".to_string(),
7858                account_id: "123456789".to_string(),
7859                role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
7860                tabs: vec![],
7861            },
7862            Session {
7863                id: "2".to_string(),
7864                timestamp: "2024-01-02".to_string(),
7865                profile: "dev-profile".to_string(),
7866                region: "eu-west-1".to_string(),
7867                account_id: "987654321".to_string(),
7868                role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
7869                tabs: vec![],
7870            },
7871        ];
7872
7873        // Filter by profile
7874        app.session_filter = "prod".to_string();
7875        let filtered = app.get_filtered_sessions();
7876        assert_eq!(filtered.len(), 1);
7877        assert_eq!(filtered[0].profile, "prod-profile");
7878
7879        // Filter by region
7880        app.session_filter = "eu".to_string();
7881        let filtered = app.get_filtered_sessions();
7882        assert_eq!(filtered.len(), 1);
7883        assert_eq!(filtered[0].region, "eu-west-1");
7884
7885        // No filter
7886        app.session_filter.clear();
7887        let filtered = app.get_filtered_sessions();
7888        assert_eq!(filtered.len(), 2);
7889    }
7890
7891    #[test]
7892    fn test_profile_picker_shows_account() {
7893        let mut app = test_app_no_region();
7894        app.available_profiles = vec![AwsProfile {
7895            name: "test-profile".to_string(),
7896            region: Some("us-east-1".to_string()),
7897            account: Some("123456789".to_string()),
7898            role_arn: None,
7899            source_profile: None,
7900        }];
7901
7902        let filtered = app.get_filtered_profiles();
7903        assert_eq!(filtered.len(), 1);
7904        assert_eq!(filtered[0].account, Some("123456789".to_string()));
7905    }
7906
7907    #[test]
7908    fn test_profile_without_account() {
7909        let mut app = test_app_no_region();
7910        app.available_profiles = vec![AwsProfile {
7911            name: "test-profile".to_string(),
7912            region: Some("us-east-1".to_string()),
7913            account: None,
7914            role_arn: None,
7915            source_profile: None,
7916        }];
7917
7918        let filtered = app.get_filtered_profiles();
7919        assert_eq!(filtered.len(), 1);
7920        assert_eq!(filtered[0].account, None);
7921    }
7922
7923    #[test]
7924    fn test_profile_with_all_fields() {
7925        let mut app = test_app_no_region();
7926        app.available_profiles = vec![AwsProfile {
7927            name: "prod-profile".to_string(),
7928            region: Some("us-west-2".to_string()),
7929            account: Some("123456789".to_string()),
7930            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
7931            source_profile: Some("base-profile".to_string()),
7932        }];
7933
7934        let filtered = app.get_filtered_profiles();
7935        assert_eq!(filtered.len(), 1);
7936        assert_eq!(filtered[0].name, "prod-profile");
7937        assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
7938        assert_eq!(filtered[0].account, Some("123456789".to_string()));
7939        assert_eq!(
7940            filtered[0].role_arn,
7941            Some("arn:aws:iam::123456789:role/AdminRole".to_string())
7942        );
7943        assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
7944    }
7945
7946    #[test]
7947    fn test_profile_filter_by_source_profile() {
7948        let mut app = test_app_no_region();
7949        app.available_profiles = vec![
7950            AwsProfile {
7951                name: "profile1".to_string(),
7952                region: None,
7953                account: None,
7954                role_arn: None,
7955                source_profile: Some("base".to_string()),
7956            },
7957            AwsProfile {
7958                name: "profile2".to_string(),
7959                region: None,
7960                account: None,
7961                role_arn: None,
7962                source_profile: Some("other".to_string()),
7963            },
7964        ];
7965
7966        app.profile_filter = "base".to_string();
7967        let filtered = app.get_filtered_profiles();
7968        assert_eq!(filtered.len(), 1);
7969        assert_eq!(filtered[0].name, "profile1");
7970    }
7971
7972    #[test]
7973    fn test_profile_filter_by_role() {
7974        let mut app = test_app_no_region();
7975        app.available_profiles = vec![
7976            AwsProfile {
7977                name: "admin-profile".to_string(),
7978                region: None,
7979                account: None,
7980                role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
7981                source_profile: None,
7982            },
7983            AwsProfile {
7984                name: "dev-profile".to_string(),
7985                region: None,
7986                account: None,
7987                role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
7988                source_profile: None,
7989            },
7990        ];
7991
7992        app.profile_filter = "Admin".to_string();
7993        let filtered = app.get_filtered_profiles();
7994        assert_eq!(filtered.len(), 1);
7995        assert_eq!(filtered[0].name, "admin-profile");
7996    }
7997
7998    #[test]
7999    fn test_profiles_sorted_by_name() {
8000        let mut app = test_app_no_region();
8001        app.available_profiles = vec![
8002            AwsProfile {
8003                name: "zebra-profile".to_string(),
8004                region: None,
8005                account: None,
8006                role_arn: None,
8007                source_profile: None,
8008            },
8009            AwsProfile {
8010                name: "alpha-profile".to_string(),
8011                region: None,
8012                account: None,
8013                role_arn: None,
8014                source_profile: None,
8015            },
8016            AwsProfile {
8017                name: "beta-profile".to_string(),
8018                region: None,
8019                account: None,
8020                role_arn: None,
8021                source_profile: None,
8022            },
8023        ];
8024
8025        let filtered = app.get_filtered_profiles();
8026        assert_eq!(filtered.len(), 3);
8027        assert_eq!(filtered[0].name, "alpha-profile");
8028        assert_eq!(filtered[1].name, "beta-profile");
8029        assert_eq!(filtered[2].name, "zebra-profile");
8030    }
8031
8032    #[test]
8033    fn test_profile_with_role_arn() {
8034        let mut app = test_app_no_region();
8035        app.available_profiles = vec![AwsProfile {
8036            name: "role-profile".to_string(),
8037            region: Some("us-east-1".to_string()),
8038            account: Some("123456789".to_string()),
8039            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
8040            source_profile: None,
8041        }];
8042
8043        let filtered = app.get_filtered_profiles();
8044        assert_eq!(filtered.len(), 1);
8045        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
8046    }
8047
8048    #[test]
8049    fn test_profile_with_user_arn() {
8050        let mut app = test_app_no_region();
8051        app.available_profiles = vec![AwsProfile {
8052            name: "user-profile".to_string(),
8053            region: Some("us-east-1".to_string()),
8054            account: Some("123456789".to_string()),
8055            role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
8056            source_profile: None,
8057        }];
8058
8059        let filtered = app.get_filtered_profiles();
8060        assert_eq!(filtered.len(), 1);
8061        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
8062    }
8063
8064    #[test]
8065    fn test_filtered_profiles_also_sorted() {
8066        let mut app = test_app_no_region();
8067        app.available_profiles = vec![
8068            AwsProfile {
8069                name: "prod-zebra".to_string(),
8070                region: Some("us-east-1".to_string()),
8071                account: None,
8072                role_arn: None,
8073                source_profile: None,
8074            },
8075            AwsProfile {
8076                name: "prod-alpha".to_string(),
8077                region: Some("us-east-1".to_string()),
8078                account: None,
8079                role_arn: None,
8080                source_profile: None,
8081            },
8082            AwsProfile {
8083                name: "dev-profile".to_string(),
8084                region: Some("us-west-2".to_string()),
8085                account: None,
8086                role_arn: None,
8087                source_profile: None,
8088            },
8089        ];
8090
8091        app.profile_filter = "prod".to_string();
8092        let filtered = app.get_filtered_profiles();
8093        assert_eq!(filtered.len(), 2);
8094        assert_eq!(filtered[0].name, "prod-alpha");
8095        assert_eq!(filtered[1].name, "prod-zebra");
8096    }
8097
8098    #[test]
8099    fn test_profile_picker_has_all_columns() {
8100        let mut app = test_app_no_region();
8101        app.available_profiles = vec![AwsProfile {
8102            name: "test".to_string(),
8103            region: Some("us-east-1".to_string()),
8104            account: Some("123456789".to_string()),
8105            role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
8106            source_profile: Some("base".to_string()),
8107        }];
8108
8109        let filtered = app.get_filtered_profiles();
8110        assert_eq!(filtered.len(), 1);
8111        assert!(filtered[0].name == "test");
8112        assert!(filtered[0].region.is_some());
8113        assert!(filtered[0].account.is_some());
8114        assert!(filtered[0].role_arn.is_some());
8115        assert!(filtered[0].source_profile.is_some());
8116    }
8117
8118    #[test]
8119    fn test_session_picker_shows_tab_count() {
8120        let mut app = test_app_no_region();
8121        app.sessions = vec![Session {
8122            id: "1".to_string(),
8123            timestamp: "2024-01-01".to_string(),
8124            profile: "test".to_string(),
8125            region: "us-east-1".to_string(),
8126            account_id: "123".to_string(),
8127            role_arn: String::new(),
8128            tabs: vec![
8129                SessionTab {
8130                    service: "CloudWatch".to_string(),
8131                    title: "Logs".to_string(),
8132                    breadcrumb: String::new(),
8133                    filter: None,
8134                    selected_item: None,
8135                },
8136                SessionTab {
8137                    service: "S3".to_string(),
8138                    title: "Buckets".to_string(),
8139                    breadcrumb: String::new(),
8140                    filter: None,
8141                    selected_item: None,
8142                },
8143            ],
8144        }];
8145
8146        let filtered = app.get_filtered_sessions();
8147        assert_eq!(filtered.len(), 1);
8148        assert_eq!(filtered[0].tabs.len(), 2);
8149    }
8150
8151    #[test]
8152    fn test_start_background_data_fetch_loads_profiles() {
8153        let mut app = test_app_no_region();
8154        assert!(app.available_profiles.is_empty());
8155
8156        // Load profiles synchronously
8157        app.available_profiles = App::load_aws_profiles();
8158
8159        // Profiles should be loaded
8160        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
8161    }
8162
8163    #[test]
8164    fn test_refresh_in_profile_picker() {
8165        let mut app = test_app_no_region();
8166        app.mode = Mode::ProfilePicker;
8167        app.available_profiles = vec![AwsProfile {
8168            name: "test".to_string(),
8169            region: None,
8170            account: None,
8171            role_arn: None,
8172            source_profile: None,
8173        }];
8174
8175        app.handle_action(Action::Refresh);
8176
8177        // Should set loading state
8178        assert!(app.log_groups_state.loading);
8179        assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
8180    }
8181
8182    #[test]
8183    fn test_refresh_sets_loading_for_profile_picker() {
8184        let mut app = test_app_no_region();
8185        app.mode = Mode::ProfilePicker;
8186
8187        assert!(!app.log_groups_state.loading);
8188
8189        app.handle_action(Action::Refresh);
8190
8191        assert!(app.log_groups_state.loading);
8192    }
8193
8194    #[test]
8195    fn test_profiles_loaded_on_demand() {
8196        let mut app = test_app_no_region();
8197
8198        // Profiles not loaded by default
8199        assert!(app.available_profiles.is_empty());
8200
8201        // Load on demand
8202        app.available_profiles = App::load_aws_profiles();
8203
8204        // Now loaded
8205        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
8206    }
8207
8208    #[test]
8209    fn test_profile_accounts_not_fetched_automatically() {
8210        let mut app = test_app_no_region();
8211        app.available_profiles = App::load_aws_profiles();
8212
8213        // Accounts should not be populated automatically
8214        for profile in &app.available_profiles {
8215            // Account may or may not be set depending on what's in config
8216            // But we're not fetching them automatically
8217            assert!(profile.account.is_none() || profile.account.is_some());
8218        }
8219    }
8220
8221    #[test]
8222    fn test_ctrl_r_triggers_account_fetch() {
8223        let mut app = test_app_no_region();
8224        app.mode = Mode::ProfilePicker;
8225        app.available_profiles = vec![AwsProfile {
8226            name: "test".to_string(),
8227            region: Some("us-east-1".to_string()),
8228            account: None,
8229            role_arn: None,
8230            source_profile: None,
8231        }];
8232
8233        // Before refresh, account is None
8234        assert!(app.available_profiles[0].account.is_none());
8235
8236        // Trigger refresh
8237        app.handle_action(Action::Refresh);
8238
8239        // Should set loading state (actual fetch happens in main.rs event loop)
8240        assert!(app.log_groups_state.loading);
8241    }
8242
8243    #[test]
8244    fn test_refresh_in_region_picker() {
8245        let mut app = test_app_no_region();
8246        app.mode = Mode::RegionPicker;
8247
8248        let initial_latencies = app.region_latencies.len();
8249        app.handle_action(Action::Refresh);
8250
8251        // Latencies should be cleared and remeasured
8252        assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
8253    }
8254
8255    #[test]
8256    fn test_refresh_in_session_picker() {
8257        let mut app = test_app_no_region();
8258        app.mode = Mode::SessionPicker;
8259        app.sessions = vec![];
8260
8261        app.handle_action(Action::Refresh);
8262
8263        // Sessions should be reloaded (may be empty if no saved sessions)
8264        assert!(app.sessions.is_empty() || !app.sessions.is_empty());
8265    }
8266
8267    #[test]
8268    fn test_session_picker_selection() {
8269        let mut app = test_app();
8270
8271        app.sessions = vec![Session {
8272            id: "1".to_string(),
8273            timestamp: "2024-01-01".to_string(),
8274            profile: "prod-profile".to_string(),
8275            region: "us-west-2".to_string(),
8276            account_id: "123456789".to_string(),
8277            role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
8278            tabs: vec![SessionTab {
8279                service: "CloudWatchLogGroups".to_string(),
8280                title: "Log Groups".to_string(),
8281                breadcrumb: "CloudWatch > Log Groups".to_string(),
8282                filter: Some("test".to_string()),
8283                selected_item: None,
8284            }],
8285        }];
8286
8287        app.mode = Mode::SessionPicker;
8288        app.session_picker_selected = 0;
8289
8290        // Simulate selecting the session
8291        app.handle_action(Action::Select);
8292
8293        assert_eq!(app.mode, Mode::Normal);
8294        assert_eq!(app.profile, "prod-profile");
8295        assert_eq!(app.region, "us-west-2");
8296        assert_eq!(app.config.account_id, "123456789");
8297        assert_eq!(app.tabs.len(), 1);
8298        assert_eq!(app.tabs[0].title, "Log Groups");
8299    }
8300
8301    #[test]
8302    fn test_save_session_creates_session() {
8303        let mut app =
8304            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8305        app.config.account_id = "123456789".to_string();
8306        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
8307
8308        app.tabs.push(Tab {
8309            service: Service::CloudWatchLogGroups,
8310            title: "Log Groups".to_string(),
8311            breadcrumb: "CloudWatch > Log Groups".to_string(),
8312        });
8313
8314        app.save_current_session();
8315
8316        assert!(app.current_session.is_some());
8317        let session = app.current_session.clone().unwrap();
8318        assert_eq!(session.profile, "test-profile");
8319        assert_eq!(session.region, "us-east-1");
8320        assert_eq!(session.account_id, "123456789");
8321        assert_eq!(session.tabs.len(), 1);
8322
8323        // Cleanup
8324        let _ = session.delete();
8325    }
8326
8327    #[test]
8328    fn test_save_session_updates_existing() {
8329        let mut app =
8330            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8331        app.config.account_id = "123456789".to_string();
8332        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
8333
8334        app.current_session = Some(Session {
8335            id: "existing".to_string(),
8336            timestamp: "2024-01-01".to_string(),
8337            profile: "test-profile".to_string(),
8338            region: "us-east-1".to_string(),
8339            account_id: "123456789".to_string(),
8340            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
8341            tabs: vec![],
8342        });
8343
8344        app.tabs.push(Tab {
8345            service: Service::CloudWatchLogGroups,
8346            title: "Log Groups".to_string(),
8347            breadcrumb: "CloudWatch > Log Groups".to_string(),
8348        });
8349
8350        app.save_current_session();
8351
8352        let session = app.current_session.clone().unwrap();
8353        assert_eq!(session.id, "existing");
8354        assert_eq!(session.tabs.len(), 1);
8355
8356        // Cleanup
8357        let _ = session.delete();
8358    }
8359
8360    #[test]
8361    fn test_save_session_skips_empty_tabs() {
8362        let mut app =
8363            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8364        app.config.account_id = "123456789".to_string();
8365
8366        app.save_current_session();
8367
8368        assert!(app.current_session.is_none());
8369    }
8370
8371    #[test]
8372    fn test_save_session_deletes_when_tabs_closed() {
8373        let mut app =
8374            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8375        app.config.account_id = "123456789".to_string();
8376        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
8377
8378        // Create a session with tabs
8379        app.current_session = Some(Session {
8380            id: "test_delete".to_string(),
8381            timestamp: "2024-01-01 10:00:00 UTC".to_string(),
8382            profile: "test-profile".to_string(),
8383            region: "us-east-1".to_string(),
8384            account_id: "123456789".to_string(),
8385            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
8386            tabs: vec![],
8387        });
8388
8389        // Save with no tabs should delete session
8390        app.save_current_session();
8391
8392        assert!(app.current_session.is_none());
8393    }
8394
8395    #[test]
8396    fn test_closing_all_tabs_deletes_session() {
8397        let mut app =
8398            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
8399        app.config.account_id = "123456789".to_string();
8400        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
8401
8402        // Add a tab
8403        app.tabs.push(Tab {
8404            service: Service::CloudWatchLogGroups,
8405            title: "Log Groups".to_string(),
8406            breadcrumb: "CloudWatch > Log Groups".to_string(),
8407        });
8408
8409        // Create session
8410        app.save_current_session();
8411        assert!(app.current_session.is_some());
8412        let session_id = app.current_session.as_ref().unwrap().id.clone();
8413
8414        // Close all tabs
8415        app.tabs.clear();
8416
8417        // Save should delete session
8418        app.save_current_session();
8419        assert!(app.current_session.is_none());
8420
8421        // Cleanup - ensure session file is deleted
8422        let _ = Session::load(&session_id).map(|s| s.delete());
8423    }
8424
8425    #[test]
8426    fn test_credential_error_opens_profile_picker() {
8427        // Simulate what main.rs does on credential error
8428        let mut app = App::new_without_client("default".to_string(), None);
8429        let error_str = "Unable to load credentials from any source";
8430
8431        if error_str.contains("credentials") {
8432            app.available_profiles = App::load_aws_profiles();
8433            app.mode = Mode::ProfilePicker;
8434        }
8435
8436        assert_eq!(app.mode, Mode::ProfilePicker);
8437        // Should have loaded profiles
8438        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
8439    }
8440
8441    #[test]
8442    fn test_non_credential_error_shows_error_modal() {
8443        let mut app = App::new_without_client("default".to_string(), None);
8444        let error_str = "Network timeout";
8445
8446        if !error_str.contains("credentials") {
8447            app.error_message = Some(error_str.to_string());
8448            app.mode = Mode::ErrorModal;
8449        }
8450
8451        assert_eq!(app.mode, Mode::ErrorModal);
8452        assert!(app.error_message.is_some());
8453    }
8454
8455    #[tokio::test]
8456    async fn test_profile_selection_loads_credentials() {
8457        // Set a valid AWS profile if available
8458        std::env::set_var("AWS_PROFILE", "default");
8459
8460        // Try to create app with profile
8461        let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
8462
8463        if let Ok(app) = result {
8464            // If credentials are available, verify they're loaded
8465            assert!(!app.config.account_id.is_empty());
8466            assert!(!app.config.role_arn.is_empty());
8467            assert_eq!(app.profile, "default");
8468            assert_eq!(app.config.region, "us-east-1");
8469        }
8470        // If no credentials, test passes (can't test without real AWS creds)
8471    }
8472
8473    #[test]
8474    fn test_new_app_shows_service_picker_with_no_tabs() {
8475        let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
8476
8477        // Should start with no service selected
8478        assert!(!app.service_selected);
8479        // Should be in ServicePicker mode (service picker)
8480        assert_eq!(app.mode, Mode::ServicePicker);
8481        // Should have no tabs
8482        assert!(app.tabs.is_empty());
8483    }
8484
8485    #[tokio::test]
8486    async fn test_aws_profile_env_var_read_before_config_load() {
8487        // This test verifies the bug: AWS_PROFILE should be read and used
8488        std::env::set_var("AWS_PROFILE", "test-profile");
8489
8490        // Simulate what happens in App::new
8491        let profile_name = None
8492            .or_else(|| std::env::var("AWS_PROFILE").ok())
8493            .unwrap_or_else(|| "default".to_string());
8494
8495        // Should have read test-profile from env
8496        assert_eq!(profile_name, "test-profile");
8497
8498        // Now set it (redundant but that's what the code does)
8499        std::env::set_var("AWS_PROFILE", &profile_name);
8500
8501        // Verify it's still set
8502        assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
8503
8504        std::env::remove_var("AWS_PROFILE");
8505    }
8506
8507    #[test]
8508    fn test_next_preferences_cloudformation() {
8509        let mut app = test_app();
8510        app.current_service = Service::CloudFormationStacks;
8511        app.mode = Mode::ColumnSelector;
8512        app.column_selector_index = 0;
8513
8514        // Should jump to PageSize section
8515        let page_size_idx = app.cfn_column_ids.len() + 2;
8516        app.handle_action(Action::NextPreferences);
8517        assert_eq!(app.column_selector_index, page_size_idx);
8518
8519        // Should wrap back to Columns
8520        app.handle_action(Action::NextPreferences);
8521        assert_eq!(app.column_selector_index, 0);
8522    }
8523
8524    #[test]
8525    fn test_next_preferences_lambda_functions() {
8526        let mut app = test_app();
8527        app.current_service = Service::LambdaFunctions;
8528        app.mode = Mode::ColumnSelector;
8529        app.column_selector_index = 0;
8530
8531        let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
8532        app.handle_action(Action::NextPreferences);
8533        assert_eq!(app.column_selector_index, page_size_idx);
8534
8535        app.handle_action(Action::NextPreferences);
8536        assert_eq!(app.column_selector_index, 0);
8537    }
8538
8539    #[test]
8540    fn test_next_preferences_lambda_applications() {
8541        let mut app = test_app();
8542        app.current_service = Service::LambdaApplications;
8543        app.mode = Mode::ColumnSelector;
8544        app.column_selector_index = 0;
8545
8546        let page_size_idx = app.lambda_application_column_ids.len() + 2;
8547        app.handle_action(Action::NextPreferences);
8548        assert_eq!(app.column_selector_index, page_size_idx);
8549
8550        app.handle_action(Action::NextPreferences);
8551        assert_eq!(app.column_selector_index, 0);
8552    }
8553
8554    #[test]
8555    fn test_next_preferences_ecr_images() {
8556        let mut app = test_app();
8557        app.current_service = Service::EcrRepositories;
8558        app.ecr_state.current_repository = Some("test-repo".to_string());
8559        app.mode = Mode::ColumnSelector;
8560        app.column_selector_index = 0;
8561
8562        let page_size_idx = app.ecr_image_column_ids.len() + 2;
8563        app.handle_action(Action::NextPreferences);
8564        assert_eq!(app.column_selector_index, page_size_idx);
8565
8566        app.handle_action(Action::NextPreferences);
8567        assert_eq!(app.column_selector_index, 0);
8568    }
8569
8570    #[test]
8571    fn test_cloudformation_next_item() {
8572        let mut app = test_app();
8573        app.current_service = Service::CloudFormationStacks;
8574        app.service_selected = true;
8575        app.mode = Mode::Normal;
8576        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8577        app.cfn_state.table.items = vec![
8578            CfnStack {
8579                name: "stack1".to_string(),
8580                stack_id: "id1".to_string(),
8581                status: "CREATE_COMPLETE".to_string(),
8582                created_time: "2024-01-01".to_string(),
8583                updated_time: String::new(),
8584                deleted_time: String::new(),
8585                drift_status: String::new(),
8586                last_drift_check_time: String::new(),
8587                status_reason: String::new(),
8588                description: String::new(),
8589                detailed_status: String::new(),
8590                root_stack: String::new(),
8591                parent_stack: String::new(),
8592                termination_protection: false,
8593                iam_role: String::new(),
8594                tags: Vec::new(),
8595                stack_policy: String::new(),
8596                rollback_monitoring_time: String::new(),
8597                rollback_alarms: Vec::new(),
8598                notification_arns: Vec::new(),
8599            },
8600            CfnStack {
8601                name: "stack2".to_string(),
8602                stack_id: "id2".to_string(),
8603                status: "UPDATE_COMPLETE".to_string(),
8604                created_time: "2024-01-02".to_string(),
8605                updated_time: String::new(),
8606                deleted_time: String::new(),
8607                drift_status: String::new(),
8608                last_drift_check_time: String::new(),
8609                status_reason: String::new(),
8610                description: String::new(),
8611                detailed_status: String::new(),
8612                root_stack: String::new(),
8613                parent_stack: String::new(),
8614                termination_protection: false,
8615                iam_role: String::new(),
8616                tags: Vec::new(),
8617                stack_policy: String::new(),
8618                rollback_monitoring_time: String::new(),
8619                rollback_alarms: Vec::new(),
8620                notification_arns: Vec::new(),
8621            },
8622        ];
8623        app.cfn_state.table.reset();
8624
8625        app.handle_action(Action::NextItem);
8626        assert_eq!(app.cfn_state.table.selected, 1);
8627
8628        app.handle_action(Action::NextItem);
8629        assert_eq!(app.cfn_state.table.selected, 1); // At max
8630    }
8631
8632    #[test]
8633    fn test_cloudformation_prev_item() {
8634        let mut app = test_app();
8635        app.current_service = Service::CloudFormationStacks;
8636        app.service_selected = true;
8637        app.mode = Mode::Normal;
8638        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8639        app.cfn_state.table.items = vec![
8640            CfnStack {
8641                name: "stack1".to_string(),
8642                stack_id: "id1".to_string(),
8643                status: "CREATE_COMPLETE".to_string(),
8644                created_time: "2024-01-01".to_string(),
8645                updated_time: String::new(),
8646                deleted_time: String::new(),
8647                drift_status: String::new(),
8648                last_drift_check_time: String::new(),
8649                status_reason: String::new(),
8650                description: String::new(),
8651                detailed_status: String::new(),
8652                root_stack: String::new(),
8653                parent_stack: String::new(),
8654                termination_protection: false,
8655                iam_role: String::new(),
8656                tags: Vec::new(),
8657                stack_policy: String::new(),
8658                rollback_monitoring_time: String::new(),
8659                rollback_alarms: Vec::new(),
8660                notification_arns: Vec::new(),
8661            },
8662            CfnStack {
8663                name: "stack2".to_string(),
8664                stack_id: "id2".to_string(),
8665                status: "UPDATE_COMPLETE".to_string(),
8666                created_time: "2024-01-02".to_string(),
8667                updated_time: String::new(),
8668                deleted_time: String::new(),
8669                drift_status: String::new(),
8670                last_drift_check_time: String::new(),
8671                status_reason: String::new(),
8672                description: String::new(),
8673                detailed_status: String::new(),
8674                root_stack: String::new(),
8675                parent_stack: String::new(),
8676                termination_protection: false,
8677                iam_role: String::new(),
8678                tags: Vec::new(),
8679                stack_policy: String::new(),
8680                rollback_monitoring_time: String::new(),
8681                rollback_alarms: Vec::new(),
8682                notification_arns: Vec::new(),
8683            },
8684        ];
8685        app.cfn_state.table.selected = 1;
8686
8687        app.handle_action(Action::PrevItem);
8688        assert_eq!(app.cfn_state.table.selected, 0);
8689
8690        app.handle_action(Action::PrevItem);
8691        assert_eq!(app.cfn_state.table.selected, 0); // At min
8692    }
8693
8694    #[test]
8695    fn test_cloudformation_page_down() {
8696        let mut app = test_app();
8697        app.current_service = Service::CloudFormationStacks;
8698        app.service_selected = true;
8699        app.mode = Mode::Normal;
8700        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8701
8702        // Create 20 stacks
8703        for i in 0..20 {
8704            app.cfn_state.table.items.push(CfnStack {
8705                name: format!("stack{}", i),
8706                stack_id: format!("id{}", i),
8707                status: "CREATE_COMPLETE".to_string(),
8708                created_time: format!("2024-01-{:02}", i + 1),
8709                updated_time: String::new(),
8710                deleted_time: String::new(),
8711                drift_status: String::new(),
8712                last_drift_check_time: String::new(),
8713                status_reason: String::new(),
8714                description: String::new(),
8715                detailed_status: String::new(),
8716                root_stack: String::new(),
8717                parent_stack: String::new(),
8718                termination_protection: false,
8719                iam_role: String::new(),
8720                tags: Vec::new(),
8721                stack_policy: String::new(),
8722                rollback_monitoring_time: String::new(),
8723                rollback_alarms: Vec::new(),
8724                notification_arns: Vec::new(),
8725            });
8726        }
8727        app.cfn_state.table.reset();
8728
8729        app.handle_action(Action::PageDown);
8730        assert_eq!(app.cfn_state.table.selected, 10);
8731
8732        app.handle_action(Action::PageDown);
8733        assert_eq!(app.cfn_state.table.selected, 19); // Clamped to max
8734    }
8735
8736    #[test]
8737    fn test_cloudformation_page_up() {
8738        let mut app = test_app();
8739        app.current_service = Service::CloudFormationStacks;
8740        app.service_selected = true;
8741        app.mode = Mode::Normal;
8742        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8743
8744        // Create 20 stacks
8745        for i in 0..20 {
8746            app.cfn_state.table.items.push(CfnStack {
8747                name: format!("stack{}", i),
8748                stack_id: format!("id{}", i),
8749                status: "CREATE_COMPLETE".to_string(),
8750                created_time: format!("2024-01-{:02}", i + 1),
8751                updated_time: String::new(),
8752                deleted_time: String::new(),
8753                drift_status: String::new(),
8754                last_drift_check_time: String::new(),
8755                status_reason: String::new(),
8756                description: String::new(),
8757                detailed_status: String::new(),
8758                root_stack: String::new(),
8759                parent_stack: String::new(),
8760                termination_protection: false,
8761                iam_role: String::new(),
8762                tags: Vec::new(),
8763                stack_policy: String::new(),
8764                rollback_monitoring_time: String::new(),
8765                rollback_alarms: Vec::new(),
8766                notification_arns: Vec::new(),
8767            });
8768        }
8769        app.cfn_state.table.selected = 15;
8770
8771        app.handle_action(Action::PageUp);
8772        assert_eq!(app.cfn_state.table.selected, 5);
8773
8774        app.handle_action(Action::PageUp);
8775        assert_eq!(app.cfn_state.table.selected, 0); // Clamped to min
8776    }
8777
8778    #[test]
8779    fn test_cloudformation_filter_input() {
8780        let mut app = test_app();
8781        app.current_service = Service::CloudFormationStacks;
8782        app.service_selected = true;
8783        app.mode = Mode::Normal;
8784
8785        app.handle_action(Action::StartFilter);
8786        assert_eq!(app.mode, Mode::FilterInput);
8787
8788        // Directly set filter (character input is handled in event loop, not actions)
8789        app.cfn_state.table.filter = "test".to_string();
8790        assert_eq!(app.cfn_state.table.filter, "test");
8791    }
8792
8793    #[test]
8794    fn test_cloudformation_filter_applies() {
8795        let mut app = test_app();
8796        app.current_service = Service::CloudFormationStacks;
8797        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8798        app.cfn_state.table.items = vec![
8799            CfnStack {
8800                name: "prod-stack".to_string(),
8801                stack_id: "id1".to_string(),
8802                status: "CREATE_COMPLETE".to_string(),
8803                created_time: "2024-01-01".to_string(),
8804                updated_time: String::new(),
8805                deleted_time: String::new(),
8806                drift_status: String::new(),
8807                last_drift_check_time: String::new(),
8808                status_reason: String::new(),
8809                description: "Production stack".to_string(),
8810                detailed_status: String::new(),
8811                root_stack: String::new(),
8812                parent_stack: String::new(),
8813                termination_protection: false,
8814                iam_role: String::new(),
8815                tags: Vec::new(),
8816                stack_policy: String::new(),
8817                rollback_monitoring_time: String::new(),
8818                rollback_alarms: Vec::new(),
8819                notification_arns: Vec::new(),
8820            },
8821            CfnStack {
8822                name: "dev-stack".to_string(),
8823                stack_id: "id2".to_string(),
8824                status: "UPDATE_COMPLETE".to_string(),
8825                created_time: "2024-01-02".to_string(),
8826                updated_time: String::new(),
8827                deleted_time: String::new(),
8828                drift_status: String::new(),
8829                last_drift_check_time: String::new(),
8830                status_reason: String::new(),
8831                description: "Development stack".to_string(),
8832                detailed_status: String::new(),
8833                root_stack: String::new(),
8834                parent_stack: String::new(),
8835                termination_protection: false,
8836                iam_role: String::new(),
8837                tags: Vec::new(),
8838                stack_policy: String::new(),
8839                rollback_monitoring_time: String::new(),
8840                rollback_alarms: Vec::new(),
8841                notification_arns: Vec::new(),
8842            },
8843        ];
8844        app.cfn_state.table.filter = "prod".to_string();
8845
8846        let filtered = app.filtered_cloudformation_stacks();
8847        assert_eq!(filtered.len(), 1);
8848        assert_eq!(filtered[0].name, "prod-stack");
8849    }
8850
8851    #[test]
8852    fn test_cloudformation_right_arrow_expands() {
8853        let mut app = test_app();
8854        app.current_service = Service::CloudFormationStacks;
8855        app.service_selected = true;
8856        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8857        app.cfn_state.table.items = vec![CfnStack {
8858            name: "test-stack".to_string(),
8859            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8860                .to_string(),
8861            status: "CREATE_COMPLETE".to_string(),
8862            created_time: "2024-01-01".to_string(),
8863            updated_time: String::new(),
8864            deleted_time: String::new(),
8865            drift_status: String::new(),
8866            last_drift_check_time: String::new(),
8867            status_reason: String::new(),
8868            description: "Test stack".to_string(),
8869            detailed_status: String::new(),
8870            root_stack: String::new(),
8871            parent_stack: String::new(),
8872            termination_protection: false,
8873            iam_role: String::new(),
8874            tags: Vec::new(),
8875            stack_policy: String::new(),
8876            rollback_monitoring_time: String::new(),
8877            rollback_alarms: Vec::new(),
8878            notification_arns: Vec::new(),
8879        }];
8880        app.cfn_state.table.reset();
8881
8882        assert_eq!(app.cfn_state.table.expanded_item, None);
8883
8884        app.handle_action(Action::NextPane);
8885        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
8886    }
8887
8888    #[test]
8889    fn test_cloudformation_left_arrow_collapses() {
8890        let mut app = test_app();
8891        app.current_service = Service::CloudFormationStacks;
8892        app.service_selected = true;
8893        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8894        app.cfn_state.table.items = vec![CfnStack {
8895            name: "test-stack".to_string(),
8896            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8897                .to_string(),
8898            status: "CREATE_COMPLETE".to_string(),
8899            created_time: "2024-01-01".to_string(),
8900            updated_time: String::new(),
8901            deleted_time: String::new(),
8902            drift_status: String::new(),
8903            last_drift_check_time: String::new(),
8904            status_reason: String::new(),
8905            description: "Test stack".to_string(),
8906            detailed_status: String::new(),
8907            root_stack: String::new(),
8908            parent_stack: String::new(),
8909            termination_protection: false,
8910            iam_role: String::new(),
8911            tags: Vec::new(),
8912            stack_policy: String::new(),
8913            rollback_monitoring_time: String::new(),
8914            rollback_alarms: Vec::new(),
8915            notification_arns: Vec::new(),
8916        }];
8917        app.cfn_state.table.reset();
8918        app.cfn_state.table.expanded_item = Some(0);
8919
8920        app.handle_action(Action::PrevPane);
8921        assert_eq!(app.cfn_state.table.expanded_item, None);
8922    }
8923
8924    #[test]
8925    fn test_cloudformation_enter_drills_into_stack() {
8926        let mut app = test_app();
8927        app.current_service = Service::CloudFormationStacks;
8928        app.service_selected = true;
8929        app.mode = Mode::Normal;
8930        app.tabs = vec![Tab {
8931            service: Service::CloudFormationStacks,
8932            title: "CloudFormation > Stacks".to_string(),
8933            breadcrumb: "CloudFormation > Stacks".to_string(),
8934        }];
8935        app.current_tab = 0;
8936        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8937        app.cfn_state.table.items = vec![CfnStack {
8938            name: "test-stack".to_string(),
8939            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8940                .to_string(),
8941            status: "CREATE_COMPLETE".to_string(),
8942            created_time: "2024-01-01".to_string(),
8943            updated_time: String::new(),
8944            deleted_time: String::new(),
8945            drift_status: String::new(),
8946            last_drift_check_time: String::new(),
8947            status_reason: String::new(),
8948            description: "Test stack".to_string(),
8949            detailed_status: String::new(),
8950            root_stack: String::new(),
8951            parent_stack: String::new(),
8952            termination_protection: false,
8953            iam_role: String::new(),
8954            tags: Vec::new(),
8955            stack_policy: String::new(),
8956            rollback_monitoring_time: String::new(),
8957            rollback_alarms: Vec::new(),
8958            notification_arns: Vec::new(),
8959        }];
8960        app.cfn_state.table.reset();
8961
8962        // Verify filtering works
8963        let filtered = app.filtered_cloudformation_stacks();
8964        assert_eq!(filtered.len(), 1);
8965        assert_eq!(filtered[0].name, "test-stack");
8966
8967        assert_eq!(app.cfn_state.current_stack, None);
8968
8969        // Enter drills into stack detail view
8970        app.handle_action(Action::Select);
8971        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
8972    }
8973
8974    #[test]
8975    fn test_cloudformation_copy_to_clipboard() {
8976        let mut app = test_app();
8977        app.current_service = Service::CloudFormationStacks;
8978        app.service_selected = true;
8979        app.mode = Mode::Normal;
8980        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8981        app.cfn_state.table.items = vec![
8982            CfnStack {
8983                name: "stack1".to_string(),
8984                stack_id: "id1".to_string(),
8985                status: "CREATE_COMPLETE".to_string(),
8986                created_time: "2024-01-01".to_string(),
8987                updated_time: String::new(),
8988                deleted_time: String::new(),
8989                drift_status: String::new(),
8990                last_drift_check_time: String::new(),
8991                status_reason: String::new(),
8992                description: String::new(),
8993                detailed_status: String::new(),
8994                root_stack: String::new(),
8995                parent_stack: String::new(),
8996                termination_protection: false,
8997                iam_role: String::new(),
8998                tags: Vec::new(),
8999                stack_policy: String::new(),
9000                rollback_monitoring_time: String::new(),
9001                rollback_alarms: Vec::new(),
9002                notification_arns: Vec::new(),
9003            },
9004            CfnStack {
9005                name: "stack2".to_string(),
9006                stack_id: "id2".to_string(),
9007                status: "UPDATE_COMPLETE".to_string(),
9008                created_time: "2024-01-02".to_string(),
9009                updated_time: String::new(),
9010                deleted_time: String::new(),
9011                drift_status: String::new(),
9012                last_drift_check_time: String::new(),
9013                status_reason: String::new(),
9014                description: String::new(),
9015                detailed_status: String::new(),
9016                root_stack: String::new(),
9017                parent_stack: String::new(),
9018                termination_protection: false,
9019                iam_role: String::new(),
9020                tags: Vec::new(),
9021                stack_policy: String::new(),
9022                rollback_monitoring_time: String::new(),
9023                rollback_alarms: Vec::new(),
9024                notification_arns: Vec::new(),
9025            },
9026        ];
9027
9028        assert!(!app.snapshot_requested);
9029        app.handle_action(Action::CopyToClipboard);
9030
9031        // Should set snapshot_requested flag
9032        assert!(app.snapshot_requested);
9033    }
9034
9035    #[test]
9036    fn test_cloudformation_expansion_shows_all_visible_columns() {
9037        let mut app = test_app();
9038        app.current_service = Service::CloudFormationStacks;
9039        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
9040        app.cfn_state.table.items = vec![CfnStack {
9041            name: "test-stack".to_string(),
9042            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
9043                .to_string(),
9044            status: "CREATE_COMPLETE".to_string(),
9045            created_time: "2024-01-01".to_string(),
9046            updated_time: "2024-01-02".to_string(),
9047            deleted_time: String::new(),
9048            drift_status: "IN_SYNC".to_string(),
9049            last_drift_check_time: "2024-01-03".to_string(),
9050            status_reason: String::new(),
9051            description: "Test description".to_string(),
9052            detailed_status: String::new(),
9053            root_stack: String::new(),
9054            parent_stack: String::new(),
9055            termination_protection: false,
9056            iam_role: String::new(),
9057            tags: Vec::new(),
9058            stack_policy: String::new(),
9059            rollback_monitoring_time: String::new(),
9060            rollback_alarms: Vec::new(),
9061            notification_arns: Vec::new(),
9062        }];
9063
9064        // Set visible columns
9065        app.cfn_visible_column_ids = [
9066            CfnColumn::Name,
9067            CfnColumn::Status,
9068            CfnColumn::CreatedTime,
9069            CfnColumn::Description,
9070        ]
9071        .iter()
9072        .map(|c| c.id())
9073        .collect();
9074
9075        app.cfn_state.table.expanded_item = Some(0);
9076
9077        // Verify all visible columns would be shown in expansion
9078        // (This is a structural test - actual rendering is in UI layer)
9079        assert_eq!(app.cfn_visible_column_ids.len(), 4);
9080        assert!(app.cfn_state.table.has_expanded_item());
9081    }
9082
9083    #[test]
9084    fn test_cloudformation_empty_list_shows_page_1() {
9085        let mut app = test_app();
9086        app.current_service = Service::CloudFormationStacks;
9087        app.cfn_state.table.items = vec![];
9088
9089        let filtered = app.filtered_cloudformation_stacks();
9090        assert_eq!(filtered.len(), 0);
9091
9092        // Pagination should still show [1] even with 0 items
9093        let page_size = app.cfn_state.table.page_size.value();
9094        let total_pages = filtered.len().div_ceil(page_size);
9095        assert_eq!(total_pages, 0);
9096
9097        // render_pagination_text(0, 0) should return "[1]"
9098        // This is tested in UI layer
9099    }
9100}
9101
9102impl App {
9103    pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
9104        let mut all = AwsRegion::all();
9105
9106        // Add latencies to regions
9107        for region in &mut all {
9108            region.latency_ms = self.region_latencies.get(region.code).copied();
9109        }
9110
9111        // Filter by search term
9112        let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
9113            all
9114        } else {
9115            let filter_lower = self.region_filter.to_lowercase();
9116            all.into_iter()
9117                .filter(|r| {
9118                    r.name.to_lowercase().contains(&filter_lower)
9119                        || r.code.to_lowercase().contains(&filter_lower)
9120                        || r.group.to_lowercase().contains(&filter_lower)
9121                })
9122                .collect()
9123        };
9124
9125        // Sort by latency (lowest first), treat None as 1000ms
9126        let mut sorted = filtered;
9127        sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
9128        sorted
9129    }
9130
9131    pub fn measure_region_latencies(&mut self) {
9132        use std::time::Instant;
9133        self.region_latencies.clear();
9134
9135        let regions = AwsRegion::all();
9136        let start_all = Instant::now();
9137        tracing::info!("Starting latency measurement for {} regions", regions.len());
9138
9139        let handles: Vec<_> = regions
9140            .iter()
9141            .map(|region| {
9142                let code = region.code.to_string();
9143                std::thread::spawn(move || {
9144                    // Use STS endpoint - fastest and most reliable
9145                    let endpoint = format!("https://sts.{}.amazonaws.com", code);
9146                    let start = Instant::now();
9147
9148                    match ureq::get(&endpoint)
9149                        .timeout(std::time::Duration::from_secs(2))
9150                        .call()
9151                    {
9152                        Ok(_) => {
9153                            let latency = start.elapsed().as_millis() as u64;
9154                            Some((code, latency))
9155                        }
9156                        Err(e) => {
9157                            tracing::debug!("Failed to measure {}: {}", code, e);
9158                            Some((code, 9999))
9159                        }
9160                    }
9161                })
9162            })
9163            .collect();
9164
9165        for handle in handles {
9166            if let Ok(Some((code, latency))) = handle.join() {
9167                self.region_latencies.insert(code, latency);
9168            }
9169        }
9170
9171        tracing::info!(
9172            "Measured {} regions in {:?}",
9173            self.region_latencies.len(),
9174            start_all.elapsed()
9175        );
9176    }
9177
9178    pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
9179        crate::aws::filter_profiles(&self.available_profiles, &self.profile_filter)
9180    }
9181
9182    pub fn get_filtered_sessions(&self) -> Vec<&Session> {
9183        if self.session_filter.is_empty() {
9184            return self.sessions.iter().collect();
9185        }
9186        let filter_lower = self.session_filter.to_lowercase();
9187        self.sessions
9188            .iter()
9189            .filter(|s| {
9190                s.profile.to_lowercase().contains(&filter_lower)
9191                    || s.region.to_lowercase().contains(&filter_lower)
9192                    || s.account_id.to_lowercase().contains(&filter_lower)
9193                    || s.role_arn.to_lowercase().contains(&filter_lower)
9194            })
9195            .collect()
9196    }
9197
9198    pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
9199        if self.tab_filter.is_empty() {
9200            return self.tabs.iter().enumerate().collect();
9201        }
9202        let filter_lower = self.tab_filter.to_lowercase();
9203        self.tabs
9204            .iter()
9205            .enumerate()
9206            .filter(|(_, tab)| {
9207                tab.title.to_lowercase().contains(&filter_lower)
9208                    || tab.breadcrumb.to_lowercase().contains(&filter_lower)
9209            })
9210            .collect()
9211    }
9212
9213    pub fn load_aws_profiles() -> Vec<AwsProfile> {
9214        AwsProfile::load_all()
9215    }
9216
9217    pub async fn fetch_profile_accounts(&mut self) {
9218        for profile in &mut self.available_profiles {
9219            if profile.account.is_none() {
9220                let region = profile
9221                    .region
9222                    .clone()
9223                    .unwrap_or_else(|| "us-east-1".to_string());
9224                if let Ok(account) =
9225                    rusticity_core::AwsConfig::get_account_for_profile(&profile.name, &region).await
9226                {
9227                    profile.account = Some(account);
9228                }
9229            }
9230        }
9231    }
9232
9233    fn save_current_session(&mut self) {
9234        // If no tabs, delete the session if it exists
9235        if self.tabs.is_empty() {
9236            if let Some(ref session) = self.current_session {
9237                let _ = session.delete();
9238                self.current_session = None;
9239            }
9240            return;
9241        }
9242
9243        let session = if let Some(ref mut current) = self.current_session {
9244            // Update existing session
9245            current.tabs = self
9246                .tabs
9247                .iter()
9248                .map(|t| SessionTab {
9249                    service: format!("{:?}", t.service),
9250                    title: t.title.clone(),
9251                    breadcrumb: t.breadcrumb.clone(),
9252                    filter: match t.service {
9253                        Service::CloudWatchLogGroups => {
9254                            Some(self.log_groups_state.log_groups.filter.clone())
9255                        }
9256                        _ => None,
9257                    },
9258                    selected_item: None,
9259                })
9260                .collect();
9261            current.clone()
9262        } else {
9263            // Create new session
9264            let mut session = Session::new(
9265                self.profile.clone(),
9266                self.region.clone(),
9267                self.config.account_id.clone(),
9268                self.config.role_arn.clone(),
9269            );
9270            session.tabs = self
9271                .tabs
9272                .iter()
9273                .map(|t| SessionTab {
9274                    service: format!("{:?}", t.service),
9275                    title: t.title.clone(),
9276                    breadcrumb: t.breadcrumb.clone(),
9277                    filter: match t.service {
9278                        Service::CloudWatchLogGroups => {
9279                            Some(self.log_groups_state.log_groups.filter.clone())
9280                        }
9281                        _ => None,
9282                    },
9283                    selected_item: None,
9284                })
9285                .collect();
9286            self.current_session = Some(session.clone());
9287            session
9288        };
9289
9290        let _ = session.save();
9291    }
9292}
9293
9294#[cfg(test)]
9295mod iam_policy_view_tests {
9296    use super::*;
9297    use test_helpers::*;
9298
9299    #[test]
9300    fn test_enter_opens_policy_view() {
9301        let mut app = test_app();
9302        app.current_service = Service::IamRoles;
9303        app.service_selected = true;
9304        app.mode = Mode::Normal;
9305        app.view_mode = ViewMode::Detail;
9306        app.iam_state.current_role = Some("TestRole".to_string());
9307        app.iam_state.policies.items = vec![crate::iam::Policy {
9308            policy_name: "TestPolicy".to_string(),
9309            policy_type: "Inline".to_string(),
9310            attached_via: "Direct".to_string(),
9311            attached_entities: "1".to_string(),
9312            description: "Test".to_string(),
9313            creation_time: "2023-01-01".to_string(),
9314            edited_time: "2023-01-01".to_string(),
9315            policy_arn: None,
9316        }];
9317        app.iam_state.policies.reset();
9318
9319        app.handle_action(Action::Select);
9320
9321        assert_eq!(app.view_mode, ViewMode::PolicyView);
9322        assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
9323        assert_eq!(app.iam_state.policy_scroll, 0);
9324        assert!(app.iam_state.policies.loading);
9325    }
9326
9327    #[test]
9328    fn test_escape_closes_policy_view() {
9329        let mut app = test_app();
9330        app.current_service = Service::IamRoles;
9331        app.service_selected = true;
9332        app.mode = Mode::Normal;
9333        app.view_mode = ViewMode::PolicyView;
9334        app.iam_state.current_role = Some("TestRole".to_string());
9335        app.iam_state.current_policy = Some("TestPolicy".to_string());
9336        app.iam_state.policy_document = "{\n  \"test\": \"value\"\n}".to_string();
9337        app.iam_state.policy_scroll = 5;
9338
9339        app.handle_action(Action::PrevPane);
9340
9341        assert_eq!(app.view_mode, ViewMode::Detail);
9342        assert_eq!(app.iam_state.current_policy, None);
9343        assert_eq!(app.iam_state.policy_document, "");
9344        assert_eq!(app.iam_state.policy_scroll, 0);
9345    }
9346
9347    #[test]
9348    fn test_ctrl_d_scrolls_down_in_policy_view() {
9349        let mut app = test_app();
9350        app.current_service = Service::IamRoles;
9351        app.service_selected = true;
9352        app.mode = Mode::Normal;
9353        app.view_mode = ViewMode::PolicyView;
9354        app.iam_state.current_role = Some("TestRole".to_string());
9355        app.iam_state.current_policy = Some("TestPolicy".to_string());
9356        app.iam_state.policy_document = (0..100)
9357            .map(|i| format!("line {}", i))
9358            .collect::<Vec<_>>()
9359            .join("\n");
9360        app.iam_state.policy_scroll = 0;
9361
9362        app.handle_action(Action::ScrollDown);
9363
9364        assert_eq!(app.iam_state.policy_scroll, 10);
9365
9366        app.handle_action(Action::ScrollDown);
9367
9368        assert_eq!(app.iam_state.policy_scroll, 20);
9369    }
9370
9371    #[test]
9372    fn test_ctrl_u_scrolls_up_in_policy_view() {
9373        let mut app = test_app();
9374        app.current_service = Service::IamRoles;
9375        app.service_selected = true;
9376        app.mode = Mode::Normal;
9377        app.view_mode = ViewMode::PolicyView;
9378        app.iam_state.current_role = Some("TestRole".to_string());
9379        app.iam_state.current_policy = Some("TestPolicy".to_string());
9380        app.iam_state.policy_document = (0..100)
9381            .map(|i| format!("line {}", i))
9382            .collect::<Vec<_>>()
9383            .join("\n");
9384        app.iam_state.policy_scroll = 30;
9385
9386        app.handle_action(Action::ScrollUp);
9387
9388        assert_eq!(app.iam_state.policy_scroll, 20);
9389
9390        app.handle_action(Action::ScrollUp);
9391
9392        assert_eq!(app.iam_state.policy_scroll, 10);
9393    }
9394
9395    #[test]
9396    fn test_scroll_does_not_go_negative() {
9397        let mut app = test_app();
9398        app.current_service = Service::IamRoles;
9399        app.service_selected = true;
9400        app.mode = Mode::Normal;
9401        app.view_mode = ViewMode::PolicyView;
9402        app.iam_state.current_role = Some("TestRole".to_string());
9403        app.iam_state.current_policy = Some("TestPolicy".to_string());
9404        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
9405        app.iam_state.policy_scroll = 0;
9406
9407        app.handle_action(Action::ScrollUp);
9408
9409        assert_eq!(app.iam_state.policy_scroll, 0);
9410    }
9411
9412    #[test]
9413    fn test_scroll_does_not_exceed_max() {
9414        let mut app = test_app();
9415        app.current_service = Service::IamRoles;
9416        app.service_selected = true;
9417        app.mode = Mode::Normal;
9418        app.view_mode = ViewMode::PolicyView;
9419        app.iam_state.current_role = Some("TestRole".to_string());
9420        app.iam_state.current_policy = Some("TestPolicy".to_string());
9421        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
9422        app.iam_state.policy_scroll = 0;
9423
9424        app.handle_action(Action::ScrollDown);
9425
9426        assert_eq!(app.iam_state.policy_scroll, 2); // Max is 2 (3 lines - 1)
9427    }
9428
9429    #[test]
9430    fn test_policy_view_console_url() {
9431        let mut app = test_app();
9432        app.current_service = Service::IamRoles;
9433        app.service_selected = true;
9434        app.view_mode = ViewMode::PolicyView;
9435        app.iam_state.current_role = Some("TestRole".to_string());
9436        app.iam_state.current_policy = Some("TestPolicy".to_string());
9437
9438        let url = app.get_console_url();
9439
9440        assert!(url.contains("us-east-1.console.aws.amazon.com"));
9441        assert!(url.contains("/roles/details/TestRole"));
9442        assert!(url.contains("/editPolicy/TestPolicy"));
9443        assert!(url.contains("step=addPermissions"));
9444    }
9445
9446    #[test]
9447    fn test_esc_from_policy_view_goes_to_role_detail() {
9448        let mut app = test_app();
9449        app.current_service = Service::IamRoles;
9450        app.service_selected = true;
9451        app.mode = Mode::Normal;
9452        app.view_mode = ViewMode::PolicyView;
9453        app.iam_state.current_role = Some("TestRole".to_string());
9454        app.iam_state.current_policy = Some("TestPolicy".to_string());
9455        app.iam_state.policy_document = "test".to_string();
9456        app.iam_state.policy_scroll = 5;
9457
9458        app.handle_action(Action::GoBack);
9459
9460        assert_eq!(app.view_mode, ViewMode::Detail);
9461        assert_eq!(app.iam_state.current_policy, None);
9462        assert_eq!(app.iam_state.policy_document, "");
9463        assert_eq!(app.iam_state.policy_scroll, 0);
9464        assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
9465    }
9466
9467    #[test]
9468    fn test_esc_from_role_detail_goes_to_role_list() {
9469        let mut app = test_app();
9470        app.current_service = Service::IamRoles;
9471        app.service_selected = true;
9472        app.mode = Mode::Normal;
9473        app.view_mode = ViewMode::Detail;
9474        app.iam_state.current_role = Some("TestRole".to_string());
9475
9476        app.handle_action(Action::GoBack);
9477
9478        assert_eq!(app.iam_state.current_role, None);
9479    }
9480
9481    #[test]
9482    fn test_right_arrow_expands_policy_row() {
9483        let mut app = test_app();
9484        app.current_service = Service::IamRoles;
9485        app.service_selected = true;
9486        app.mode = Mode::Normal;
9487        app.view_mode = ViewMode::Detail;
9488        app.iam_state.current_role = Some("TestRole".to_string());
9489        app.iam_state.policies.items = vec![crate::iam::Policy {
9490            policy_name: "TestPolicy".to_string(),
9491            policy_type: "Inline".to_string(),
9492            attached_via: "Direct".to_string(),
9493            attached_entities: "1".to_string(),
9494            description: "Test".to_string(),
9495            creation_time: "2023-01-01".to_string(),
9496            edited_time: "2023-01-01".to_string(),
9497            policy_arn: None,
9498        }];
9499        app.iam_state.policies.reset();
9500
9501        app.handle_action(Action::NextPane);
9502
9503        // Should expand, not drill down
9504        assert_eq!(app.view_mode, ViewMode::Detail);
9505        assert_eq!(app.iam_state.current_policy, None);
9506        assert_eq!(app.iam_state.policies.expanded_item, Some(0));
9507    }
9508}
9509
9510#[cfg(test)]
9511mod tab_filter_tests {
9512    use super::*;
9513    use test_helpers::*;
9514
9515    #[test]
9516    fn test_space_t_opens_tab_picker() {
9517        let mut app = test_app();
9518        app.tabs = vec![
9519            Tab {
9520                service: Service::CloudWatchLogGroups,
9521                title: "Tab 1".to_string(),
9522                breadcrumb: "CloudWatch > Log groups".to_string(),
9523            },
9524            Tab {
9525                service: Service::S3Buckets,
9526                title: "Tab 2".to_string(),
9527                breadcrumb: "S3 > Buckets".to_string(),
9528            },
9529        ];
9530        app.current_tab = 0;
9531
9532        app.handle_action(Action::OpenTabPicker);
9533
9534        assert_eq!(app.mode, Mode::TabPicker);
9535        assert_eq!(app.tab_picker_selected, 0);
9536    }
9537
9538    #[test]
9539    fn test_tab_filter_works() {
9540        let mut app = test_app();
9541        app.tabs = vec![
9542            Tab {
9543                service: Service::CloudWatchLogGroups,
9544                title: "CloudWatch Logs".to_string(),
9545                breadcrumb: "CloudWatch > Log groups".to_string(),
9546            },
9547            Tab {
9548                service: Service::S3Buckets,
9549                title: "S3 Buckets".to_string(),
9550                breadcrumb: "S3 > Buckets".to_string(),
9551            },
9552            Tab {
9553                service: Service::CloudWatchAlarms,
9554                title: "CloudWatch Alarms".to_string(),
9555                breadcrumb: "CloudWatch > Alarms".to_string(),
9556            },
9557        ];
9558        app.mode = Mode::TabPicker;
9559
9560        // Filter for "s3"
9561        app.handle_action(Action::FilterInput('s'));
9562        app.handle_action(Action::FilterInput('3'));
9563
9564        let filtered = app.get_filtered_tabs();
9565        assert_eq!(filtered.len(), 1);
9566        assert_eq!(filtered[0].1.title, "S3 Buckets");
9567    }
9568
9569    #[test]
9570    fn test_tab_filter_by_breadcrumb() {
9571        let mut app = test_app();
9572        app.tabs = vec![
9573            Tab {
9574                service: Service::CloudWatchLogGroups,
9575                title: "Tab 1".to_string(),
9576                breadcrumb: "CloudWatch > Log groups".to_string(),
9577            },
9578            Tab {
9579                service: Service::S3Buckets,
9580                title: "Tab 2".to_string(),
9581                breadcrumb: "S3 > Buckets".to_string(),
9582            },
9583        ];
9584        app.mode = Mode::TabPicker;
9585
9586        // Filter for "cloudwatch"
9587        app.handle_action(Action::FilterInput('c'));
9588        app.handle_action(Action::FilterInput('l'));
9589        app.handle_action(Action::FilterInput('o'));
9590        app.handle_action(Action::FilterInput('u'));
9591        app.handle_action(Action::FilterInput('d'));
9592
9593        let filtered = app.get_filtered_tabs();
9594        assert_eq!(filtered.len(), 1);
9595        assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
9596    }
9597
9598    #[test]
9599    fn test_tab_filter_backspace() {
9600        let mut app = test_app();
9601        app.tabs = vec![
9602            Tab {
9603                service: Service::CloudWatchLogGroups,
9604                title: "CloudWatch Logs".to_string(),
9605                breadcrumb: "CloudWatch > Log groups".to_string(),
9606            },
9607            Tab {
9608                service: Service::S3Buckets,
9609                title: "S3 Buckets".to_string(),
9610                breadcrumb: "S3 > Buckets".to_string(),
9611            },
9612        ];
9613        app.mode = Mode::TabPicker;
9614
9615        app.handle_action(Action::FilterInput('s'));
9616        app.handle_action(Action::FilterInput('3'));
9617        assert_eq!(app.tab_filter, "s3");
9618
9619        app.handle_action(Action::FilterBackspace);
9620        assert_eq!(app.tab_filter, "s");
9621
9622        let filtered = app.get_filtered_tabs();
9623        assert_eq!(filtered.len(), 2); // Both match "s"
9624    }
9625
9626    #[test]
9627    fn test_tab_selection_with_filter() {
9628        let mut app = test_app();
9629        app.tabs = vec![
9630            Tab {
9631                service: Service::CloudWatchLogGroups,
9632                title: "CloudWatch Logs".to_string(),
9633                breadcrumb: "CloudWatch > Log groups".to_string(),
9634            },
9635            Tab {
9636                service: Service::S3Buckets,
9637                title: "S3 Buckets".to_string(),
9638                breadcrumb: "S3 > Buckets".to_string(),
9639            },
9640        ];
9641        app.mode = Mode::TabPicker;
9642        app.current_tab = 0;
9643
9644        // Filter for "s3"
9645        app.handle_action(Action::FilterInput('s'));
9646        app.handle_action(Action::FilterInput('3'));
9647
9648        // Select the filtered tab
9649        app.handle_action(Action::Select);
9650
9651        assert_eq!(app.current_tab, 1); // Should select the S3 tab (index 1)
9652        assert_eq!(app.mode, Mode::Normal);
9653        assert_eq!(app.tab_filter, ""); // Filter should be cleared
9654    }
9655}
9656
9657#[cfg(test)]
9658mod region_latency_tests {
9659    use super::*;
9660    use test_helpers::*;
9661
9662    #[test]
9663    fn test_regions_sorted_by_latency() {
9664        let mut app = test_app();
9665
9666        // Add some latencies
9667        app.region_latencies.insert("us-west-2".to_string(), 50);
9668        app.region_latencies.insert("us-east-1".to_string(), 10);
9669        app.region_latencies.insert("eu-west-1".to_string(), 100);
9670
9671        let filtered = app.get_filtered_regions();
9672
9673        // Should be sorted by latency (lowest first)
9674        let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
9675
9676        assert!(with_latency.len() >= 3);
9677        assert_eq!(with_latency[0].code, "us-east-1");
9678        assert_eq!(with_latency[0].latency_ms, Some(10));
9679        assert_eq!(with_latency[1].code, "us-west-2");
9680        assert_eq!(with_latency[1].latency_ms, Some(50));
9681        assert_eq!(with_latency[2].code, "eu-west-1");
9682        assert_eq!(with_latency[2].latency_ms, Some(100));
9683    }
9684
9685    #[test]
9686    fn test_regions_with_latency_before_without() {
9687        let mut app = test_app();
9688
9689        // Only add latency for one region
9690        app.region_latencies.insert("eu-west-1".to_string(), 100);
9691
9692        let filtered = app.get_filtered_regions();
9693
9694        // Region with latency should come first
9695        assert_eq!(filtered[0].code, "eu-west-1");
9696        assert_eq!(filtered[0].latency_ms, Some(100));
9697
9698        // Rest should be sorted by name
9699        for region in &filtered[1..] {
9700            assert!(region.latency_ms.is_none());
9701        }
9702    }
9703
9704    #[test]
9705    fn test_region_filter_with_latency() {
9706        let mut app = test_app();
9707
9708        app.region_latencies.insert("us-east-1".to_string(), 10);
9709        app.region_latencies.insert("us-west-2".to_string(), 50);
9710        app.region_filter = "us".to_string();
9711
9712        let filtered = app.get_filtered_regions();
9713
9714        // Should only have US regions, sorted by latency
9715        assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
9716        assert_eq!(filtered[0].code, "us-east-1");
9717        assert_eq!(filtered[1].code, "us-west-2");
9718    }
9719
9720    #[test]
9721    fn test_latency_persists_across_filters() {
9722        let mut app = test_app();
9723
9724        app.region_latencies.insert("us-east-1".to_string(), 10);
9725
9726        // Filter to something else
9727        app.region_filter = "eu".to_string();
9728        let filtered = app.get_filtered_regions();
9729        assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
9730
9731        // Clear filter
9732        app.region_filter.clear();
9733        let all = app.get_filtered_regions();
9734
9735        // Latency should still be there
9736        let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
9737        assert_eq!(us_east.latency_ms, Some(10));
9738    }
9739
9740    #[test]
9741    fn test_measure_region_latencies_clears_previous() {
9742        let mut app = test_app();
9743
9744        // Add some fake latencies
9745        app.region_latencies.insert("us-east-1".to_string(), 100);
9746        app.region_latencies.insert("eu-west-1".to_string(), 200);
9747
9748        // Measure again (will fail to connect but should clear)
9749        app.measure_region_latencies();
9750
9751        // Old latencies should be cleared
9752        assert!(
9753            app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
9754        );
9755    }
9756
9757    #[test]
9758    fn test_regions_with_latency_sorted_first() {
9759        let mut app = test_app();
9760
9761        // Add latencies: one fast, one slow (>1000ms would be treated as >1s)
9762        app.region_latencies.insert("us-east-1".to_string(), 50);
9763        app.region_latencies.insert("eu-west-1".to_string(), 500);
9764
9765        let filtered = app.get_filtered_regions();
9766
9767        // Should show all regions
9768        assert!(filtered.len() > 2);
9769
9770        // Fast regions first
9771        assert_eq!(filtered[0].code, "us-east-1");
9772        assert_eq!(filtered[0].latency_ms, Some(50));
9773        assert_eq!(filtered[1].code, "eu-west-1");
9774        assert_eq!(filtered[1].latency_ms, Some(500));
9775
9776        // Regions without latency treated as 1000ms, so they come after 500ms
9777        for region in &filtered[2..] {
9778            assert!(region.latency_ms.is_none());
9779        }
9780    }
9781
9782    #[test]
9783    fn test_regions_without_latency_sorted_as_1000ms() {
9784        let mut app = test_app();
9785
9786        // Add one region with 1500ms (slower than default 1000ms)
9787        app.region_latencies
9788            .insert("ap-southeast-2".to_string(), 1500);
9789        // Add one region with 50ms (faster)
9790        app.region_latencies.insert("us-east-1".to_string(), 50);
9791
9792        let filtered = app.get_filtered_regions();
9793
9794        // Fast region first
9795        assert_eq!(filtered[0].code, "us-east-1");
9796        assert_eq!(filtered[0].latency_ms, Some(50));
9797
9798        // Regions without latency (treated as 1000ms) come before 1500ms
9799        let slow_region_idx = filtered
9800            .iter()
9801            .position(|r| r.code == "ap-southeast-2")
9802            .unwrap();
9803        assert!(slow_region_idx > 1); // Should be after fast region and regions without latency
9804
9805        // All regions between index 1 and slow_region_idx should have no latency
9806        for region in filtered.iter().take(slow_region_idx).skip(1) {
9807            assert!(region.latency_ms.is_none());
9808        }
9809    }
9810
9811    #[test]
9812    fn test_region_picker_opens_with_latencies() {
9813        let mut app = test_app();
9814
9815        // Simulate opening region picker
9816        app.region_filter.clear();
9817        app.region_picker_selected = 0;
9818        app.measure_region_latencies();
9819
9820        // Should have attempted to measure (even if all fail in test env)
9821        // The map should be initialized
9822        assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
9823    }
9824
9825    #[test]
9826    fn test_ecr_tab_next() {
9827        assert_eq!(EcrTab::Private.next(), EcrTab::Public);
9828        assert_eq!(EcrTab::Public.next(), EcrTab::Private);
9829    }
9830
9831    #[test]
9832    fn test_ecr_tab_switching() {
9833        let mut app = test_app();
9834        app.current_service = Service::EcrRepositories;
9835        app.service_selected = true;
9836        app.ecr_state.tab = EcrTab::Private;
9837
9838        app.handle_action(Action::NextDetailTab);
9839        assert_eq!(app.ecr_state.tab, EcrTab::Public);
9840        assert_eq!(app.ecr_state.repositories.selected, 0);
9841
9842        app.handle_action(Action::NextDetailTab);
9843        assert_eq!(app.ecr_state.tab, EcrTab::Private);
9844    }
9845
9846    #[test]
9847    fn test_ecr_navigation() {
9848        let mut app = test_app();
9849        app.current_service = Service::EcrRepositories;
9850        app.service_selected = true;
9851        app.mode = Mode::Normal;
9852        app.ecr_state.repositories.items = vec![
9853            EcrRepository {
9854                name: "repo1".to_string(),
9855                uri: "uri1".to_string(),
9856                created_at: "2023-01-01".to_string(),
9857                tag_immutability: "MUTABLE".to_string(),
9858                encryption_type: "AES256".to_string(),
9859            },
9860            EcrRepository {
9861                name: "repo2".to_string(),
9862                uri: "uri2".to_string(),
9863                created_at: "2023-01-02".to_string(),
9864                tag_immutability: "IMMUTABLE".to_string(),
9865                encryption_type: "KMS".to_string(),
9866            },
9867        ];
9868
9869        app.handle_action(Action::NextItem);
9870        assert_eq!(app.ecr_state.repositories.selected, 1);
9871
9872        app.handle_action(Action::PrevItem);
9873        assert_eq!(app.ecr_state.repositories.selected, 0);
9874    }
9875
9876    #[test]
9877    fn test_ecr_filter() {
9878        let mut app = test_app();
9879        app.current_service = Service::EcrRepositories;
9880        app.service_selected = true;
9881        app.ecr_state.repositories.items = vec![
9882            EcrRepository {
9883                name: "my-app".to_string(),
9884                uri: "uri1".to_string(),
9885                created_at: "2023-01-01".to_string(),
9886                tag_immutability: "MUTABLE".to_string(),
9887                encryption_type: "AES256".to_string(),
9888            },
9889            EcrRepository {
9890                name: "other-service".to_string(),
9891                uri: "uri2".to_string(),
9892                created_at: "2023-01-02".to_string(),
9893                tag_immutability: "IMMUTABLE".to_string(),
9894                encryption_type: "KMS".to_string(),
9895            },
9896        ];
9897
9898        app.ecr_state.repositories.filter = "app".to_string();
9899        let filtered = app.filtered_ecr_repositories();
9900        assert_eq!(filtered.len(), 1);
9901        assert_eq!(filtered[0].name, "my-app");
9902    }
9903
9904    #[test]
9905    fn test_ecr_filter_input() {
9906        let mut app = test_app();
9907        app.current_service = Service::EcrRepositories;
9908        app.service_selected = true;
9909        app.mode = Mode::FilterInput;
9910
9911        app.handle_action(Action::FilterInput('t'));
9912        app.handle_action(Action::FilterInput('e'));
9913        app.handle_action(Action::FilterInput('s'));
9914        app.handle_action(Action::FilterInput('t'));
9915        assert_eq!(app.ecr_state.repositories.filter, "test");
9916
9917        app.handle_action(Action::FilterBackspace);
9918        assert_eq!(app.ecr_state.repositories.filter, "tes");
9919    }
9920
9921    #[test]
9922    fn test_iam_users_filter_input() {
9923        let mut app = test_app();
9924        app.current_service = Service::IamUsers;
9925        app.service_selected = true;
9926        app.mode = Mode::FilterInput;
9927
9928        app.handle_action(Action::FilterInput('a'));
9929        app.handle_action(Action::FilterInput('d'));
9930        app.handle_action(Action::FilterInput('m'));
9931        app.handle_action(Action::FilterInput('i'));
9932        app.handle_action(Action::FilterInput('n'));
9933        assert_eq!(app.iam_state.users.filter, "admin");
9934
9935        app.handle_action(Action::FilterBackspace);
9936        assert_eq!(app.iam_state.users.filter, "admi");
9937    }
9938
9939    #[test]
9940    fn test_iam_policies_filter_input() {
9941        let mut app = test_app();
9942        app.current_service = Service::IamUsers;
9943        app.service_selected = true;
9944        app.iam_state.current_user = Some("testuser".to_string());
9945        app.mode = Mode::FilterInput;
9946
9947        app.handle_action(Action::FilterInput('r'));
9948        app.handle_action(Action::FilterInput('e'));
9949        app.handle_action(Action::FilterInput('a'));
9950        app.handle_action(Action::FilterInput('d'));
9951        assert_eq!(app.iam_state.policies.filter, "read");
9952
9953        app.handle_action(Action::FilterBackspace);
9954        assert_eq!(app.iam_state.policies.filter, "rea");
9955    }
9956
9957    #[test]
9958    fn test_iam_start_filter() {
9959        let mut app = test_app();
9960        app.current_service = Service::IamUsers;
9961        app.service_selected = true;
9962        app.mode = Mode::Normal;
9963
9964        app.handle_action(Action::StartFilter);
9965        assert_eq!(app.mode, Mode::FilterInput);
9966    }
9967
9968    #[test]
9969    fn test_iam_roles_filter_input() {
9970        let mut app = test_app();
9971        app.current_service = Service::IamRoles;
9972        app.service_selected = true;
9973        app.mode = Mode::FilterInput;
9974
9975        app.handle_action(Action::FilterInput('a'));
9976        app.handle_action(Action::FilterInput('d'));
9977        app.handle_action(Action::FilterInput('m'));
9978        app.handle_action(Action::FilterInput('i'));
9979        app.handle_action(Action::FilterInput('n'));
9980        assert_eq!(app.iam_state.roles.filter, "admin");
9981
9982        app.handle_action(Action::FilterBackspace);
9983        assert_eq!(app.iam_state.roles.filter, "admi");
9984    }
9985
9986    #[test]
9987    fn test_iam_roles_start_filter() {
9988        let mut app = test_app();
9989        app.current_service = Service::IamRoles;
9990        app.service_selected = true;
9991        app.mode = Mode::Normal;
9992
9993        app.handle_action(Action::StartFilter);
9994        assert_eq!(app.mode, Mode::FilterInput);
9995    }
9996
9997    #[test]
9998    fn test_iam_roles_navigation() {
9999        let mut app = test_app();
10000        app.current_service = Service::IamRoles;
10001        app.service_selected = true;
10002        app.mode = Mode::Normal;
10003        app.iam_state.roles.items = (0..10)
10004            .map(|i| crate::iam::IamRole {
10005                role_name: format!("role{}", i),
10006                path: "/".to_string(),
10007                trusted_entities: String::new(),
10008                last_activity: String::new(),
10009                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
10010                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
10011                description: String::new(),
10012                max_session_duration: Some(3600),
10013            })
10014            .collect();
10015
10016        assert_eq!(app.iam_state.roles.selected, 0);
10017
10018        app.handle_action(Action::NextItem);
10019        assert_eq!(app.iam_state.roles.selected, 1);
10020
10021        app.handle_action(Action::NextItem);
10022        assert_eq!(app.iam_state.roles.selected, 2);
10023
10024        app.handle_action(Action::PrevItem);
10025        assert_eq!(app.iam_state.roles.selected, 1);
10026    }
10027
10028    #[test]
10029    fn test_iam_roles_page_hotkey() {
10030        let mut app = test_app();
10031        app.current_service = Service::IamRoles;
10032        app.service_selected = true;
10033        app.mode = Mode::Normal;
10034        app.iam_state.roles.page_size = PageSize::Ten;
10035        app.iam_state.roles.items = (0..100)
10036            .map(|i| crate::iam::IamRole {
10037                role_name: format!("role{}", i),
10038                path: "/".to_string(),
10039                trusted_entities: String::new(),
10040                last_activity: String::new(),
10041                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
10042                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
10043                description: String::new(),
10044                max_session_duration: Some(3600),
10045            })
10046            .collect();
10047
10048        app.handle_action(Action::FilterInput('2'));
10049        app.handle_action(Action::OpenColumnSelector);
10050        assert_eq!(app.iam_state.roles.selected, 10); // Page 2 = index 10 (with page size 10)
10051    }
10052
10053    #[test]
10054    fn test_iam_users_page_hotkey() {
10055        let mut app = test_app();
10056        app.current_service = Service::IamUsers;
10057        app.service_selected = true;
10058        app.mode = Mode::Normal;
10059        app.iam_state.users.page_size = PageSize::Ten;
10060        app.iam_state.users.items = (0..100)
10061            .map(|i| crate::iam::IamUser {
10062                user_name: format!("user{}", i),
10063                path: "/".to_string(),
10064                groups: String::new(),
10065                last_activity: String::new(),
10066                mfa: String::new(),
10067                password_age: String::new(),
10068                console_last_sign_in: String::new(),
10069                access_key_id: String::new(),
10070                active_key_age: String::new(),
10071                access_key_last_used: String::new(),
10072                arn: format!("arn:aws:iam::123456789012:user/user{}", i),
10073                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
10074                console_access: String::new(),
10075                signing_certs: String::new(),
10076            })
10077            .collect();
10078
10079        app.handle_action(Action::FilterInput('3'));
10080        app.handle_action(Action::OpenColumnSelector);
10081        assert_eq!(app.iam_state.users.selected, 20); // Page 3 = index 20 (with page size 10)
10082    }
10083
10084    #[test]
10085    fn test_ecr_scroll_navigation() {
10086        let mut app = test_app();
10087        app.current_service = Service::EcrRepositories;
10088        app.service_selected = true;
10089        app.ecr_state.repositories.items = (0..20)
10090            .map(|i| EcrRepository {
10091                name: format!("repo{}", i),
10092                uri: format!("uri{}", i),
10093                created_at: "2023-01-01".to_string(),
10094                tag_immutability: "MUTABLE".to_string(),
10095                encryption_type: "AES256".to_string(),
10096            })
10097            .collect();
10098
10099        app.handle_action(Action::ScrollDown);
10100        assert_eq!(app.ecr_state.repositories.selected, 10);
10101
10102        app.handle_action(Action::ScrollUp);
10103        assert_eq!(app.ecr_state.repositories.selected, 0);
10104    }
10105
10106    #[test]
10107    fn test_ecr_tab_switching_triggers_reload() {
10108        let mut app = test_app();
10109        app.current_service = Service::EcrRepositories;
10110        app.service_selected = true;
10111        app.ecr_state.tab = EcrTab::Private;
10112        app.ecr_state.repositories.loading = false;
10113        app.ecr_state.repositories.items = vec![EcrRepository {
10114            name: "private-repo".to_string(),
10115            uri: "uri".to_string(),
10116            created_at: "2023-01-01".to_string(),
10117            tag_immutability: "MUTABLE".to_string(),
10118            encryption_type: "AES256".to_string(),
10119        }];
10120
10121        app.handle_action(Action::NextDetailTab);
10122        assert_eq!(app.ecr_state.tab, EcrTab::Public);
10123        assert!(app.ecr_state.repositories.loading);
10124        assert_eq!(app.ecr_state.repositories.selected, 0);
10125    }
10126
10127    #[test]
10128    fn test_ecr_tab_cycles_between_private_and_public() {
10129        let mut app = test_app();
10130        app.current_service = Service::EcrRepositories;
10131        app.service_selected = true;
10132        app.ecr_state.tab = EcrTab::Private;
10133
10134        app.handle_action(Action::NextDetailTab);
10135        assert_eq!(app.ecr_state.tab, EcrTab::Public);
10136
10137        app.handle_action(Action::NextDetailTab);
10138        assert_eq!(app.ecr_state.tab, EcrTab::Private);
10139    }
10140
10141    #[test]
10142    fn test_page_size_values() {
10143        assert_eq!(PageSize::Ten.value(), 10);
10144        assert_eq!(PageSize::TwentyFive.value(), 25);
10145        assert_eq!(PageSize::Fifty.value(), 50);
10146        assert_eq!(PageSize::OneHundred.value(), 100);
10147    }
10148
10149    #[test]
10150    fn test_page_size_next() {
10151        assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
10152        assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
10153        assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
10154        assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
10155    }
10156
10157    #[test]
10158    fn test_ecr_enter_drills_into_repository() {
10159        let mut app = test_app();
10160        app.current_service = Service::EcrRepositories;
10161        app.service_selected = true;
10162        app.mode = Mode::Normal;
10163        app.ecr_state.repositories.items = vec![EcrRepository {
10164            name: "my-repo".to_string(),
10165            uri: "uri".to_string(),
10166            created_at: "2023-01-01".to_string(),
10167            tag_immutability: "MUTABLE".to_string(),
10168            encryption_type: "AES256".to_string(),
10169        }];
10170
10171        app.handle_action(Action::Select);
10172        assert_eq!(
10173            app.ecr_state.current_repository,
10174            Some("my-repo".to_string())
10175        );
10176        assert!(app.ecr_state.repositories.loading);
10177    }
10178
10179    #[test]
10180    fn test_ecr_repository_expansion() {
10181        let mut app = test_app();
10182        app.current_service = Service::EcrRepositories;
10183        app.service_selected = true;
10184        app.ecr_state.repositories.items = vec![EcrRepository {
10185            name: "my-repo".to_string(),
10186            uri: "uri".to_string(),
10187            created_at: "2023-01-01".to_string(),
10188            tag_immutability: "MUTABLE".to_string(),
10189            encryption_type: "AES256".to_string(),
10190        }];
10191        app.ecr_state.repositories.selected = 0;
10192
10193        assert_eq!(app.ecr_state.repositories.expanded_item, None);
10194
10195        app.handle_action(Action::NextPane);
10196        assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
10197
10198        app.handle_action(Action::PrevPane);
10199        assert_eq!(app.ecr_state.repositories.expanded_item, None);
10200    }
10201
10202    #[test]
10203    fn test_ecr_ctrl_d_scrolls_down() {
10204        let mut app = test_app();
10205        app.current_service = Service::EcrRepositories;
10206        app.service_selected = true;
10207        app.mode = Mode::Normal;
10208        app.ecr_state.repositories.items = (0..30)
10209            .map(|i| EcrRepository {
10210                name: format!("repo{}", i),
10211                uri: format!("uri{}", i),
10212                created_at: "2023-01-01".to_string(),
10213                tag_immutability: "MUTABLE".to_string(),
10214                encryption_type: "AES256".to_string(),
10215            })
10216            .collect();
10217        app.ecr_state.repositories.selected = 0;
10218
10219        app.handle_action(Action::PageDown);
10220        assert_eq!(app.ecr_state.repositories.selected, 10);
10221    }
10222
10223    #[test]
10224    fn test_ecr_ctrl_u_scrolls_up() {
10225        let mut app = test_app();
10226        app.current_service = Service::EcrRepositories;
10227        app.service_selected = true;
10228        app.mode = Mode::Normal;
10229        app.ecr_state.repositories.items = (0..30)
10230            .map(|i| EcrRepository {
10231                name: format!("repo{}", i),
10232                uri: format!("uri{}", i),
10233                created_at: "2023-01-01".to_string(),
10234                tag_immutability: "MUTABLE".to_string(),
10235                encryption_type: "AES256".to_string(),
10236            })
10237            .collect();
10238        app.ecr_state.repositories.selected = 15;
10239
10240        app.handle_action(Action::PageUp);
10241        assert_eq!(app.ecr_state.repositories.selected, 5);
10242    }
10243
10244    #[test]
10245    fn test_ecr_images_ctrl_d_scrolls_down() {
10246        let mut app = test_app();
10247        app.current_service = Service::EcrRepositories;
10248        app.service_selected = true;
10249        app.mode = Mode::Normal;
10250        app.ecr_state.current_repository = Some("repo".to_string());
10251        app.ecr_state.images.items = (0..30)
10252            .map(|i| EcrImage {
10253                tag: format!("tag{}", i),
10254                artifact_type: "container".to_string(),
10255                pushed_at: "2023-01-01T12:00:00Z".to_string(),
10256                size_bytes: 104857600,
10257                uri: format!("uri{}", i),
10258                digest: format!("sha256:{}", i),
10259                last_pull_time: String::new(),
10260            })
10261            .collect();
10262        app.ecr_state.images.selected = 0;
10263
10264        app.handle_action(Action::PageDown);
10265        assert_eq!(app.ecr_state.images.selected, 10);
10266    }
10267
10268    #[test]
10269    fn test_ecr_esc_goes_back_from_images_to_repos() {
10270        let mut app = test_app();
10271        app.current_service = Service::EcrRepositories;
10272        app.service_selected = true;
10273        app.mode = Mode::Normal;
10274        app.ecr_state.current_repository = Some("my-repo".to_string());
10275        app.ecr_state.images.items = vec![EcrImage {
10276            tag: "latest".to_string(),
10277            artifact_type: "container".to_string(),
10278            pushed_at: "2023-01-01T12:00:00Z".to_string(),
10279            size_bytes: 104857600,
10280            uri: "uri".to_string(),
10281            digest: "sha256:abc".to_string(),
10282            last_pull_time: String::new(),
10283        }];
10284
10285        app.handle_action(Action::GoBack);
10286        assert_eq!(app.ecr_state.current_repository, None);
10287        assert!(app.ecr_state.images.items.is_empty());
10288    }
10289
10290    #[test]
10291    fn test_ecr_esc_collapses_expanded_image_first() {
10292        let mut app = test_app();
10293        app.current_service = Service::EcrRepositories;
10294        app.service_selected = true;
10295        app.mode = Mode::Normal;
10296        app.ecr_state.current_repository = Some("my-repo".to_string());
10297        app.ecr_state.images.expanded_item = Some(0);
10298
10299        app.handle_action(Action::GoBack);
10300        assert_eq!(app.ecr_state.images.expanded_item, None);
10301        assert_eq!(
10302            app.ecr_state.current_repository,
10303            Some("my-repo".to_string())
10304        );
10305    }
10306
10307    #[test]
10308    fn test_pagination_with_lowercase_p() {
10309        let mut app = test_app();
10310        app.current_service = Service::EcrRepositories;
10311        app.service_selected = true;
10312        app.mode = Mode::Normal;
10313        app.ecr_state.repositories.items = (0..100)
10314            .map(|i| EcrRepository {
10315                name: format!("repo{}", i),
10316                uri: format!("uri{}", i),
10317                created_at: "2023-01-01".to_string(),
10318                tag_immutability: "MUTABLE".to_string(),
10319                encryption_type: "AES256".to_string(),
10320            })
10321            .collect();
10322
10323        // Type "2" then "p" to go to page 2
10324        app.handle_action(Action::FilterInput('2'));
10325        assert_eq!(app.page_input, "2");
10326
10327        app.handle_action(Action::OpenColumnSelector); // 'p' key
10328        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
10329        assert_eq!(app.page_input, ""); // Should be cleared
10330    }
10331
10332    #[test]
10333    fn test_lowercase_p_without_number_opens_preferences() {
10334        let mut app = test_app();
10335        app.current_service = Service::EcrRepositories;
10336        app.service_selected = true;
10337        app.mode = Mode::Normal;
10338
10339        app.handle_action(Action::OpenColumnSelector); // 'p' key without number
10340        assert_eq!(app.mode, Mode::ColumnSelector);
10341    }
10342
10343    #[test]
10344    fn test_ctrl_o_generates_correct_console_url() {
10345        let mut app = test_app();
10346        app.current_service = Service::EcrRepositories;
10347        app.service_selected = true;
10348        app.mode = Mode::Normal;
10349        app.config.account_id = "123456789012".to_string();
10350
10351        // Test repository list URL
10352        let url = app.get_console_url();
10353        assert!(url.contains("ecr/private-registry/repositories"));
10354        assert!(url.contains("region=us-east-1"));
10355
10356        // Test images URL
10357        app.ecr_state.current_repository = Some("my-repo".to_string());
10358        let url = app.get_console_url();
10359        assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
10360        assert!(url.contains("region=us-east-1"));
10361    }
10362
10363    #[test]
10364    fn test_page_input_display_and_reset() {
10365        let mut app = test_app();
10366        app.current_service = Service::EcrRepositories;
10367        app.service_selected = true;
10368        app.mode = Mode::Normal;
10369        app.ecr_state.repositories.items = (0..100)
10370            .map(|i| EcrRepository {
10371                name: format!("repo{}", i),
10372                uri: format!("uri{}", i),
10373                created_at: "2023-01-01".to_string(),
10374                tag_immutability: "MUTABLE".to_string(),
10375                encryption_type: "AES256".to_string(),
10376            })
10377            .collect();
10378
10379        // Type "2"
10380        app.handle_action(Action::FilterInput('2'));
10381        assert_eq!(app.page_input, "2");
10382
10383        // Press 'p' to go to page 2
10384        app.handle_action(Action::OpenColumnSelector);
10385        assert_eq!(app.page_input, ""); // Should be cleared
10386        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
10387    }
10388
10389    #[test]
10390    fn test_page_navigation_updates_scroll_offset_for_cfn() {
10391        let mut app = test_app();
10392        app.current_service = Service::CloudFormationStacks;
10393        app.service_selected = true;
10394        app.mode = Mode::Normal;
10395        app.cfn_state.table.items = (0..100)
10396            .map(|i| crate::cfn::Stack {
10397                name: format!("stack-{}", i),
10398                stack_id: format!(
10399                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
10400                    i
10401                ),
10402                status: "CREATE_COMPLETE".to_string(),
10403                created_time: "2023-01-01T00:00:00Z".to_string(),
10404                updated_time: "2023-01-01T00:00:00Z".to_string(),
10405                deleted_time: String::new(),
10406                drift_status: "IN_SYNC".to_string(),
10407                last_drift_check_time: String::new(),
10408                status_reason: String::new(),
10409                description: String::new(),
10410                detailed_status: String::new(),
10411                root_stack: String::new(),
10412                parent_stack: String::new(),
10413                termination_protection: false,
10414                iam_role: String::new(),
10415                tags: vec![],
10416                stack_policy: String::new(),
10417                rollback_monitoring_time: String::new(),
10418                rollback_alarms: vec![],
10419                notification_arns: vec![],
10420            })
10421            .collect();
10422
10423        // Type "2" then "p" to go to page 2
10424        app.handle_action(Action::FilterInput('2'));
10425        assert_eq!(app.page_input, "2");
10426
10427        app.handle_action(Action::OpenColumnSelector); // 'p' key
10428        assert_eq!(app.page_input, ""); // Should be cleared
10429
10430        // Verify both selected and scroll_offset are updated
10431        let page_size = app.cfn_state.table.page_size.value();
10432        let expected_offset = page_size; // Page 2 starts at page_size
10433        assert_eq!(app.cfn_state.table.selected, expected_offset);
10434        assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
10435
10436        // Verify pagination display shows page 2
10437        let current_page = app.cfn_state.table.scroll_offset / page_size;
10438        assert_eq!(
10439            current_page, 1,
10440            "2p should go to page 2 (0-indexed as 1), not page 3"
10441        ); // 0-indexed, so page 2 is index 1
10442    }
10443
10444    #[test]
10445    fn test_3p_goes_to_page_3_not_page_5() {
10446        let mut app = test_app();
10447        app.current_service = Service::CloudFormationStacks;
10448        app.service_selected = true;
10449        app.mode = Mode::Normal;
10450        app.cfn_state.table.items = (0..200)
10451            .map(|i| crate::cfn::Stack {
10452                name: format!("stack-{}", i),
10453                stack_id: format!(
10454                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
10455                    i
10456                ),
10457                status: "CREATE_COMPLETE".to_string(),
10458                created_time: "2023-01-01T00:00:00Z".to_string(),
10459                updated_time: "2023-01-01T00:00:00Z".to_string(),
10460                deleted_time: String::new(),
10461                drift_status: "IN_SYNC".to_string(),
10462                last_drift_check_time: String::new(),
10463                status_reason: String::new(),
10464                description: String::new(),
10465                detailed_status: String::new(),
10466                root_stack: String::new(),
10467                parent_stack: String::new(),
10468                termination_protection: false,
10469                iam_role: String::new(),
10470                tags: vec![],
10471                stack_policy: String::new(),
10472                rollback_monitoring_time: String::new(),
10473                rollback_alarms: vec![],
10474                notification_arns: vec![],
10475            })
10476            .collect();
10477
10478        // Type "3" then "p" to go to page 3
10479        app.handle_action(Action::FilterInput('3'));
10480        app.handle_action(Action::OpenColumnSelector);
10481
10482        let page_size = app.cfn_state.table.page_size.value();
10483        let current_page = app.cfn_state.table.scroll_offset / page_size;
10484        assert_eq!(
10485            current_page, 2,
10486            "3p should go to page 3 (0-indexed as 2), not page 5"
10487        );
10488        assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
10489    }
10490
10491    #[test]
10492    fn test_log_streams_page_navigation_uses_correct_page_size() {
10493        let mut app = test_app();
10494        app.current_service = Service::CloudWatchLogGroups;
10495        app.view_mode = ViewMode::Detail;
10496        app.service_selected = true;
10497        app.mode = Mode::Normal;
10498        app.log_groups_state.log_streams = (0..100)
10499            .map(|i| LogStream {
10500                name: format!("stream-{}", i),
10501                creation_time: None,
10502                last_event_time: None,
10503            })
10504            .collect();
10505
10506        // Type "2" then "p" to go to page 2
10507        app.handle_action(Action::FilterInput('2'));
10508        app.handle_action(Action::OpenColumnSelector);
10509
10510        // Log streams use page_size=20, so page 2 starts at index 20
10511        assert_eq!(app.log_groups_state.selected_stream, 20);
10512
10513        // Verify pagination display shows page 2 (not page 3)
10514        let page_size = 20;
10515        let current_page = app.log_groups_state.selected_stream / page_size;
10516        assert_eq!(
10517            current_page, 1,
10518            "2p should go to page 2 (0-indexed as 1), not page 3"
10519        );
10520    }
10521
10522    #[test]
10523    fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
10524        let mut app = test_app();
10525        app.current_service = Service::EcrRepositories;
10526        app.service_selected = true;
10527        app.mode = Mode::Normal;
10528        app.ecr_state.repositories.page_size = PageSize::TwentyFive; // Set to 25
10529        app.ecr_state.repositories.items = (0..100)
10530            .map(|i| EcrRepository {
10531                name: format!("repo{}", i),
10532                uri: format!("uri{}", i),
10533                created_at: "2023-01-01".to_string(),
10534                tag_immutability: "MUTABLE".to_string(),
10535                encryption_type: "AES256".to_string(),
10536            })
10537            .collect();
10538
10539        // Type "3" then "p" to go to page 3
10540        app.handle_action(Action::FilterInput('3'));
10541        app.handle_action(Action::OpenColumnSelector);
10542
10543        // With page_size=25, page 3 starts at index 50
10544        assert_eq!(app.ecr_state.repositories.selected, 50);
10545
10546        let page_size = app.ecr_state.repositories.page_size.value();
10547        let current_page = app.ecr_state.repositories.selected / page_size;
10548        assert_eq!(
10549            current_page, 2,
10550            "3p with page_size=25 should go to page 3 (0-indexed as 2)"
10551        );
10552    }
10553
10554    #[test]
10555    fn test_page_navigation_updates_scroll_offset_for_alarms() {
10556        let mut app = test_app();
10557        app.current_service = Service::CloudWatchAlarms;
10558        app.service_selected = true;
10559        app.mode = Mode::Normal;
10560        app.alarms_state.table.items = (0..100)
10561            .map(|i| crate::cw::alarms::Alarm {
10562                name: format!("alarm-{}", i),
10563                state: "OK".to_string(),
10564                state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
10565                description: String::new(),
10566                metric_name: "CPUUtilization".to_string(),
10567                namespace: "AWS/EC2".to_string(),
10568                statistic: "Average".to_string(),
10569                period: 300,
10570                comparison_operator: "GreaterThanThreshold".to_string(),
10571                threshold: 80.0,
10572                actions_enabled: true,
10573                state_reason: String::new(),
10574                resource: String::new(),
10575                dimensions: String::new(),
10576                expression: String::new(),
10577                alarm_type: "MetricAlarm".to_string(),
10578                cross_account: String::new(),
10579            })
10580            .collect();
10581
10582        // Type "2" then "p" to go to page 2
10583        app.handle_action(Action::FilterInput('2'));
10584        app.handle_action(Action::OpenColumnSelector);
10585
10586        // Verify both selected and scroll_offset are updated
10587        let page_size = app.alarms_state.table.page_size.value();
10588        let expected_offset = page_size; // Page 2 starts at page_size
10589        assert_eq!(app.alarms_state.table.selected, expected_offset);
10590        assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
10591    }
10592
10593    #[test]
10594    fn test_ecr_pagination_with_65_repos() {
10595        let mut app = test_app();
10596        app.current_service = Service::EcrRepositories;
10597        app.service_selected = true;
10598        app.mode = Mode::Normal;
10599        app.ecr_state.repositories.items = (0..65)
10600            .map(|i| EcrRepository {
10601                name: format!("repo{:02}", i),
10602                uri: format!("uri{}", i),
10603                created_at: "2023-01-01".to_string(),
10604                tag_immutability: "MUTABLE".to_string(),
10605                encryption_type: "AES256".to_string(),
10606            })
10607            .collect();
10608
10609        // Page 1: items 0-49 (50 items)
10610        assert_eq!(app.ecr_state.repositories.selected, 0);
10611        let page_size = 50;
10612        let current_page = app.ecr_state.repositories.selected / page_size;
10613        assert_eq!(current_page, 0);
10614
10615        // Go to page 2
10616        app.handle_action(Action::FilterInput('2'));
10617        app.handle_action(Action::OpenColumnSelector);
10618        assert_eq!(app.ecr_state.repositories.selected, 50);
10619
10620        // Page 2: items 50-64 (15 items)
10621        let current_page = app.ecr_state.repositories.selected / page_size;
10622        assert_eq!(current_page, 1);
10623    }
10624
10625    #[test]
10626    fn test_ecr_repos_input_focus_tab_cycling() {
10627        let mut app = test_app();
10628        app.current_service = Service::EcrRepositories;
10629        app.service_selected = true;
10630        app.mode = Mode::FilterInput;
10631        app.ecr_state.input_focus = InputFocus::Filter;
10632
10633        // Tab should cycle to Pagination
10634        app.handle_action(Action::NextFilterFocus);
10635        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10636
10637        // Tab again should cycle back to Input
10638        app.handle_action(Action::NextFilterFocus);
10639        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10640
10641        // Shift+Tab should cycle backwards to Pagination
10642        app.handle_action(Action::PrevFilterFocus);
10643        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10644
10645        // Shift+Tab again should cycle back to Input
10646        app.handle_action(Action::PrevFilterFocus);
10647        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10648    }
10649
10650    #[test]
10651    fn test_ecr_images_column_toggle_not_off_by_one() {
10652        use crate::ecr::image::Column as ImageColumn;
10653        let mut app = test_app();
10654        app.current_service = Service::EcrRepositories;
10655        app.service_selected = true;
10656        app.mode = Mode::ColumnSelector;
10657        app.ecr_state.current_repository = Some("test-repo".to_string());
10658
10659        // Start with all columns visible
10660        app.ecr_image_visible_column_ids = ImageColumn::ids();
10661        let initial_count = app.ecr_image_visible_column_ids.len();
10662
10663        // Select first column (index 0) and toggle it
10664        app.column_selector_index = 0;
10665        app.handle_action(Action::ToggleColumn);
10666
10667        // First column should be removed
10668        assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
10669        assert!(!app
10670            .ecr_image_visible_column_ids
10671            .contains(&ImageColumn::Tag.id()));
10672
10673        // Toggle it back
10674        app.handle_action(Action::ToggleColumn);
10675        assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
10676        assert!(app
10677            .ecr_image_visible_column_ids
10678            .contains(&ImageColumn::Tag.id()));
10679    }
10680
10681    #[test]
10682    fn test_ecr_repos_column_toggle_works() {
10683        let mut app = test_app();
10684        app.current_service = Service::EcrRepositories;
10685        app.service_selected = true;
10686        app.mode = Mode::ColumnSelector;
10687        app.ecr_state.current_repository = None;
10688
10689        // Start with all columns visible
10690        app.ecr_repo_visible_column_ids = EcrColumn::ids();
10691        let initial_count = app.ecr_repo_visible_column_ids.len();
10692
10693        // Select first column (index 0) and toggle it
10694        app.column_selector_index = 0;
10695        app.handle_action(Action::ToggleColumn);
10696
10697        // First column should be removed
10698        assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
10699        assert!(!app
10700            .ecr_repo_visible_column_ids
10701            .contains(&EcrColumn::Name.id()));
10702
10703        // Toggle it back
10704        app.handle_action(Action::ToggleColumn);
10705        assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
10706        assert!(app
10707            .ecr_repo_visible_column_ids
10708            .contains(&EcrColumn::Name.id()));
10709    }
10710
10711    #[test]
10712    fn test_ecr_repos_pagination_left_right_navigation() {
10713        use crate::ecr::repo::Repository as EcrRepository;
10714        let mut app = test_app();
10715        app.current_service = Service::EcrRepositories;
10716        app.service_selected = true;
10717        app.mode = Mode::FilterInput;
10718        app.ecr_state.input_focus = InputFocus::Pagination;
10719
10720        // Create 150 repos (3 pages with page size 50)
10721        app.ecr_state.repositories.items = (0..150)
10722            .map(|i| EcrRepository {
10723                name: format!("repo{:03}", i),
10724                uri: format!("uri{}", i),
10725                created_at: "2023-01-01".to_string(),
10726                tag_immutability: "MUTABLE".to_string(),
10727                encryption_type: "AES256".to_string(),
10728            })
10729            .collect();
10730
10731        // Start on page 1 (index 0)
10732        app.ecr_state.repositories.selected = 0;
10733        eprintln!(
10734            "Initial: selected={}, focus={:?}, mode={:?}",
10735            app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
10736        );
10737
10738        // Right arrow (PageDown) should go to page 2
10739        app.handle_action(Action::PageDown);
10740        eprintln!(
10741            "After PageDown: selected={}",
10742            app.ecr_state.repositories.selected
10743        );
10744        assert_eq!(app.ecr_state.repositories.selected, 50);
10745
10746        // Right arrow again should go to page 3
10747        app.handle_action(Action::PageDown);
10748        eprintln!(
10749            "After 2nd PageDown: selected={}",
10750            app.ecr_state.repositories.selected
10751        );
10752        assert_eq!(app.ecr_state.repositories.selected, 100);
10753
10754        // Right arrow at last page should stay at last page
10755        app.handle_action(Action::PageDown);
10756        eprintln!(
10757            "After 3rd PageDown: selected={}",
10758            app.ecr_state.repositories.selected
10759        );
10760        assert_eq!(app.ecr_state.repositories.selected, 100);
10761
10762        // Left arrow (PageUp) should go back to page 2
10763        app.handle_action(Action::PageUp);
10764        eprintln!(
10765            "After PageUp: selected={}",
10766            app.ecr_state.repositories.selected
10767        );
10768        assert_eq!(app.ecr_state.repositories.selected, 50);
10769
10770        // Left arrow again should go to page 1
10771        app.handle_action(Action::PageUp);
10772        eprintln!(
10773            "After 2nd PageUp: selected={}",
10774            app.ecr_state.repositories.selected
10775        );
10776        assert_eq!(app.ecr_state.repositories.selected, 0);
10777
10778        // Left arrow at first page should stay at first page
10779        app.handle_action(Action::PageUp);
10780        eprintln!(
10781            "After 3rd PageUp: selected={}",
10782            app.ecr_state.repositories.selected
10783        );
10784        assert_eq!(app.ecr_state.repositories.selected, 0);
10785    }
10786
10787    #[test]
10788    fn test_ecr_repos_filter_input_when_input_focused() {
10789        use crate::ecr::repo::Repository as EcrRepository;
10790        let mut app = test_app();
10791        app.current_service = Service::EcrRepositories;
10792        app.service_selected = true;
10793        app.mode = Mode::FilterInput;
10794        app.ecr_state.input_focus = InputFocus::Filter;
10795
10796        // Create some repos
10797        app.ecr_state.repositories.items = vec![
10798            EcrRepository {
10799                name: "test-repo".to_string(),
10800                uri: "uri1".to_string(),
10801                created_at: "2023-01-01".to_string(),
10802                tag_immutability: "MUTABLE".to_string(),
10803                encryption_type: "AES256".to_string(),
10804            },
10805            EcrRepository {
10806                name: "prod-repo".to_string(),
10807                uri: "uri2".to_string(),
10808                created_at: "2023-01-01".to_string(),
10809                tag_immutability: "MUTABLE".to_string(),
10810                encryption_type: "AES256".to_string(),
10811            },
10812        ];
10813
10814        // When input is focused, typing should add to filter
10815        assert_eq!(app.ecr_state.repositories.filter, "");
10816        app.handle_action(Action::FilterInput('t'));
10817        assert_eq!(app.ecr_state.repositories.filter, "t");
10818        app.handle_action(Action::FilterInput('e'));
10819        assert_eq!(app.ecr_state.repositories.filter, "te");
10820        app.handle_action(Action::FilterInput('s'));
10821        assert_eq!(app.ecr_state.repositories.filter, "tes");
10822        app.handle_action(Action::FilterInput('t'));
10823        assert_eq!(app.ecr_state.repositories.filter, "test");
10824    }
10825
10826    #[test]
10827    fn test_ecr_repos_digit_input_when_pagination_focused() {
10828        use crate::ecr::repo::Repository as EcrRepository;
10829        let mut app = test_app();
10830        app.current_service = Service::EcrRepositories;
10831        app.service_selected = true;
10832        app.mode = Mode::FilterInput;
10833        app.ecr_state.input_focus = InputFocus::Pagination;
10834
10835        // Create some repos
10836        app.ecr_state.repositories.items = vec![EcrRepository {
10837            name: "test-repo".to_string(),
10838            uri: "uri1".to_string(),
10839            created_at: "2023-01-01".to_string(),
10840            tag_immutability: "MUTABLE".to_string(),
10841            encryption_type: "AES256".to_string(),
10842        }];
10843
10844        // When pagination is focused, digits should go to page_input, not filter
10845        assert_eq!(app.ecr_state.repositories.filter, "");
10846        assert_eq!(app.page_input, "");
10847        app.handle_action(Action::FilterInput('2'));
10848        assert_eq!(app.ecr_state.repositories.filter, "");
10849        assert_eq!(app.page_input, "2");
10850
10851        // Non-digits should not be added to either
10852        app.handle_action(Action::FilterInput('a'));
10853        assert_eq!(app.ecr_state.repositories.filter, "");
10854        assert_eq!(app.page_input, "2");
10855    }
10856
10857    #[test]
10858    fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
10859        use crate::ecr::repo::Repository as EcrRepository;
10860        let mut app = test_app();
10861        app.current_service = Service::EcrRepositories;
10862        app.service_selected = true;
10863        app.mode = Mode::FilterInput;
10864        app.ecr_state.input_focus = InputFocus::Filter;
10865
10866        // Create 150 repos (3 pages)
10867        app.ecr_state.repositories.items = (0..150)
10868            .map(|i| EcrRepository {
10869                name: format!("repo{:03}", i),
10870                uri: format!("uri{}", i),
10871                created_at: "2023-01-01".to_string(),
10872                tag_immutability: "MUTABLE".to_string(),
10873                encryption_type: "AES256".to_string(),
10874            })
10875            .collect();
10876
10877        // Start on page 1
10878        app.ecr_state.repositories.selected = 0;
10879
10880        // When input is focused, left/right should scroll table (not change pages)
10881        app.handle_action(Action::PageDown);
10882        assert_eq!(
10883            app.ecr_state.repositories.selected, 10,
10884            "Should scroll down by 10"
10885        );
10886
10887        app.handle_action(Action::PageUp);
10888        assert_eq!(
10889            app.ecr_state.repositories.selected, 0,
10890            "Should scroll back up"
10891        );
10892    }
10893
10894    #[test]
10895    fn test_ecr_repos_pagination_control_actually_works() {
10896        use crate::ecr::repo::Repository as EcrRepository;
10897
10898        // Test that verifies the exact conditions needed for pagination to work
10899        let mut app = test_app();
10900        app.current_service = Service::EcrRepositories;
10901        app.service_selected = true;
10902        app.mode = Mode::FilterInput;
10903        app.ecr_state.current_repository = None;
10904        app.ecr_state.input_focus = InputFocus::Pagination;
10905
10906        // Create 100 repos (2 pages with page size 50)
10907        app.ecr_state.repositories.items = (0..100)
10908            .map(|i| EcrRepository {
10909                name: format!("repo{:03}", i),
10910                uri: format!("uri{}", i),
10911                created_at: "2023-01-01".to_string(),
10912                tag_immutability: "MUTABLE".to_string(),
10913                encryption_type: "AES256".to_string(),
10914            })
10915            .collect();
10916
10917        app.ecr_state.repositories.selected = 0;
10918
10919        // Verify all conditions are met
10920        assert_eq!(app.mode, Mode::FilterInput);
10921        assert_eq!(app.current_service, Service::EcrRepositories);
10922        assert_eq!(app.ecr_state.current_repository, None);
10923        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10924
10925        // Now test pagination
10926        app.handle_action(Action::PageDown);
10927        assert_eq!(
10928            app.ecr_state.repositories.selected, 50,
10929            "PageDown should move to page 2"
10930        );
10931
10932        app.handle_action(Action::PageUp);
10933        assert_eq!(
10934            app.ecr_state.repositories.selected, 0,
10935            "PageUp should move back to page 1"
10936        );
10937    }
10938
10939    #[test]
10940    fn test_ecr_repos_start_filter_resets_focus_to_input() {
10941        let mut app = test_app();
10942        app.current_service = Service::EcrRepositories;
10943        app.service_selected = true;
10944        app.mode = Mode::Normal;
10945        app.ecr_state.current_repository = None;
10946
10947        // Set focus to Pagination
10948        app.ecr_state.input_focus = InputFocus::Pagination;
10949
10950        // Start filter mode
10951        app.handle_action(Action::StartFilter);
10952
10953        // Should reset to Input focus
10954        assert_eq!(app.mode, Mode::FilterInput);
10955        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10956    }
10957
10958    #[test]
10959    fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
10960        use crate::ecr::repo::Repository as EcrRepository;
10961
10962        let mut app = test_app();
10963        app.current_service = Service::EcrRepositories;
10964        app.service_selected = true;
10965        app.mode = Mode::Normal;
10966        app.ecr_state.current_repository = None;
10967
10968        // Create 100 repos (2 pages)
10969        app.ecr_state.repositories.items = (0..100)
10970            .map(|i| EcrRepository {
10971                name: format!("repo{:03}", i),
10972                uri: format!("uri{}", i),
10973                created_at: "2023-01-01".to_string(),
10974                tag_immutability: "MUTABLE".to_string(),
10975                encryption_type: "AES256".to_string(),
10976            })
10977            .collect();
10978
10979        app.ecr_state.repositories.selected = 0;
10980
10981        // User presses 'i' to enter filter mode
10982        app.handle_action(Action::StartFilter);
10983        assert_eq!(app.mode, Mode::FilterInput);
10984        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10985
10986        // User presses Tab to switch to pagination
10987        app.handle_action(Action::NextFilterFocus);
10988        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10989
10990        // User presses right arrow (PageDown)
10991        eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
10992            app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
10993        app.handle_action(Action::PageDown);
10994        eprintln!(
10995            "After PageDown: selected={}",
10996            app.ecr_state.repositories.selected
10997        );
10998
10999        // Should move to page 2
11000        assert_eq!(
11001            app.ecr_state.repositories.selected, 50,
11002            "Right arrow should move to page 2"
11003        );
11004
11005        // User presses left arrow (PageUp)
11006        app.handle_action(Action::PageUp);
11007        assert_eq!(
11008            app.ecr_state.repositories.selected, 0,
11009            "Left arrow should move back to page 1"
11010        );
11011    }
11012
11013    #[test]
11014    fn test_service_picker_i_key_activates_filter() {
11015        let mut app = test_app();
11016
11017        // Start in ServicePicker mode (service picker)
11018        assert_eq!(app.mode, Mode::ServicePicker);
11019        assert!(app.service_picker.filter.is_empty());
11020
11021        // Press 'i' to start filtering
11022        app.handle_action(Action::FilterInput('i'));
11023
11024        // Should still be in ServicePicker mode and filter should have 'i'
11025        assert_eq!(app.mode, Mode::ServicePicker);
11026        assert_eq!(app.service_picker.filter, "i");
11027    }
11028
11029    #[test]
11030    fn test_service_picker_typing_filters_services() {
11031        let mut app = test_app();
11032
11033        // Start in ServicePicker mode
11034        assert_eq!(app.mode, Mode::ServicePicker);
11035
11036        // Type "s3" to filter
11037        app.handle_action(Action::FilterInput('s'));
11038        app.handle_action(Action::FilterInput('3'));
11039
11040        assert_eq!(app.service_picker.filter, "s3");
11041        assert_eq!(app.mode, Mode::ServicePicker);
11042    }
11043
11044    #[test]
11045    fn test_service_picker_resets_on_open() {
11046        let mut app = test_app();
11047
11048        // Select a service to get into Normal mode
11049        app.service_selected = true;
11050        app.mode = Mode::Normal;
11051
11052        // Simulate having previous filter and selection
11053        app.service_picker.filter = "previous".to_string();
11054        app.service_picker.selected = 5;
11055
11056        // Open space menu (service picker)
11057        app.handle_action(Action::OpenSpaceMenu);
11058
11059        // Filter and selection should be reset
11060        assert_eq!(app.mode, Mode::SpaceMenu);
11061        assert!(app.service_picker.filter.is_empty());
11062        assert_eq!(app.service_picker.selected, 0);
11063    }
11064
11065    #[test]
11066    fn test_no_pii_in_test_data() {
11067        // Ensure test data uses placeholder account IDs, not real ones
11068        let test_repo = EcrRepository {
11069            name: "test-repo".to_string(),
11070            uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
11071            created_at: "2024-01-01".to_string(),
11072            tag_immutability: "MUTABLE".to_string(),
11073            encryption_type: "AES256".to_string(),
11074        };
11075
11076        // Verify placeholder account ID is used
11077        assert!(test_repo.uri.starts_with("123456789012"));
11078        assert!(!test_repo.uri.contains("123456789013")); // Not a real account
11079    }
11080
11081    #[test]
11082    fn test_lambda_versions_tab_triggers_loading() {
11083        let mut app = test_app();
11084        app.current_service = Service::LambdaFunctions;
11085        app.service_selected = true;
11086
11087        // Simulate selecting a function
11088        app.lambda_state.current_function = Some("test-function".to_string());
11089        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11090
11091        // Initially no versions
11092        assert!(app.lambda_state.version_table.items.is_empty());
11093
11094        // Switch to Versions tab
11095        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11096
11097        // The main loop should detect this change and load versions
11098        // We verify the state is set up correctly for loading
11099        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11100        assert!(app.lambda_state.current_function.is_some());
11101    }
11102
11103    #[test]
11104    fn test_lambda_versions_navigation() {
11105        use crate::lambda::Version;
11106
11107        let mut app = test_app();
11108        app.current_service = Service::LambdaFunctions;
11109        app.service_selected = true;
11110        app.lambda_state.current_function = Some("test-function".to_string());
11111        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11112
11113        // Add test versions
11114        app.lambda_state.version_table.items = vec![
11115            Version {
11116                version: "3".to_string(),
11117                aliases: "prod".to_string(),
11118                description: "".to_string(),
11119                last_modified: "".to_string(),
11120                architecture: "X86_64".to_string(),
11121            },
11122            Version {
11123                version: "2".to_string(),
11124                aliases: "".to_string(),
11125                description: "".to_string(),
11126                last_modified: "".to_string(),
11127                architecture: "X86_64".to_string(),
11128            },
11129            Version {
11130                version: "1".to_string(),
11131                aliases: "".to_string(),
11132                description: "".to_string(),
11133                last_modified: "".to_string(),
11134                architecture: "X86_64".to_string(),
11135            },
11136        ];
11137
11138        // Verify versions are loaded
11139        assert_eq!(app.lambda_state.version_table.items.len(), 3);
11140        assert_eq!(app.lambda_state.version_table.items[0].version, "3");
11141        assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
11142
11143        // Verify selection can be changed
11144        app.lambda_state.version_table.selected = 1;
11145        assert_eq!(app.lambda_state.version_table.selected, 1);
11146    }
11147
11148    #[test]
11149    fn test_lambda_versions_with_aliases() {
11150        use crate::lambda::Version;
11151
11152        let version = Version {
11153            version: "35".to_string(),
11154            aliases: "prod, staging".to_string(),
11155            description: "Production version".to_string(),
11156            last_modified: "2024-01-01".to_string(),
11157            architecture: "X86_64".to_string(),
11158        };
11159
11160        assert_eq!(version.aliases, "prod, staging");
11161        assert!(!version.aliases.is_empty());
11162    }
11163
11164    #[test]
11165    fn test_lambda_versions_expansion() {
11166        use crate::lambda::Version;
11167
11168        let mut app = test_app();
11169        app.current_service = Service::LambdaFunctions;
11170        app.service_selected = true;
11171        app.lambda_state.current_function = Some("test-function".to_string());
11172        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11173
11174        // Add test versions
11175        app.lambda_state.version_table.items = vec![
11176            Version {
11177                version: "2".to_string(),
11178                aliases: "prod".to_string(),
11179                description: "Production".to_string(),
11180                last_modified: "2024-01-01".to_string(),
11181                architecture: "X86_64".to_string(),
11182            },
11183            Version {
11184                version: "1".to_string(),
11185                aliases: "".to_string(),
11186                description: "".to_string(),
11187                last_modified: "2024-01-01".to_string(),
11188                architecture: "Arm64".to_string(),
11189            },
11190        ];
11191
11192        app.lambda_state.version_table.selected = 0;
11193
11194        // Verify expansion can be set
11195        app.lambda_state.version_table.expanded_item = Some(0);
11196        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11197
11198        // Select different version
11199        app.lambda_state.version_table.selected = 1;
11200        app.lambda_state.version_table.expanded_item = Some(1);
11201        assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
11202    }
11203
11204    #[test]
11205    fn test_lambda_versions_page_navigation() {
11206        use crate::lambda::Version;
11207
11208        let mut app = test_app();
11209        app.current_service = Service::LambdaFunctions;
11210        app.service_selected = true;
11211        app.lambda_state.current_function = Some("test-function".to_string());
11212        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11213
11214        // Add 30 test versions
11215        app.lambda_state.version_table.items = (1..=30)
11216            .map(|i| Version {
11217                version: i.to_string(),
11218                aliases: "".to_string(),
11219                description: "".to_string(),
11220                last_modified: "".to_string(),
11221                architecture: "X86_64".to_string(),
11222            })
11223            .collect();
11224
11225        app.lambda_state.version_table.page_size = PageSize::Ten;
11226        app.lambda_state.version_table.selected = 0;
11227
11228        // Go to page 2
11229        app.page_input = "2".to_string();
11230        app.handle_action(Action::OpenColumnSelector);
11231
11232        // Should be at index 10 (start of page 2)
11233        assert_eq!(app.lambda_state.version_table.selected, 10);
11234    }
11235
11236    #[test]
11237    fn test_lambda_versions_pagination_arrow_keys() {
11238        use crate::lambda::Version;
11239
11240        let mut app = test_app();
11241        app.current_service = Service::LambdaFunctions;
11242        app.service_selected = true;
11243        app.lambda_state.current_function = Some("test-function".to_string());
11244        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11245        app.mode = Mode::FilterInput;
11246        app.lambda_state.version_input_focus = InputFocus::Pagination;
11247
11248        // Add 30 test versions
11249        app.lambda_state.version_table.items = (1..=30)
11250            .map(|i| Version {
11251                version: i.to_string(),
11252                aliases: "".to_string(),
11253                description: "".to_string(),
11254                last_modified: "".to_string(),
11255                architecture: "X86_64".to_string(),
11256            })
11257            .collect();
11258
11259        app.lambda_state.version_table.page_size = PageSize::Ten;
11260        app.lambda_state.version_table.selected = 0;
11261
11262        // Right arrow (PageDown) should go to next page
11263        app.handle_action(Action::PageDown);
11264        assert_eq!(app.lambda_state.version_table.selected, 10);
11265
11266        // Left arrow (PageUp) should go back
11267        app.handle_action(Action::PageUp);
11268        assert_eq!(app.lambda_state.version_table.selected, 0);
11269    }
11270
11271    #[test]
11272    fn test_lambda_versions_page_input_in_filter_mode() {
11273        use crate::lambda::Version;
11274
11275        let mut app = test_app();
11276        app.current_service = Service::LambdaFunctions;
11277        app.service_selected = true;
11278        app.lambda_state.current_function = Some("test-function".to_string());
11279        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11280        app.mode = Mode::FilterInput;
11281        app.lambda_state.version_input_focus = InputFocus::Pagination;
11282
11283        // Add 30 test versions
11284        app.lambda_state.version_table.items = (1..=30)
11285            .map(|i| Version {
11286                version: i.to_string(),
11287                aliases: "".to_string(),
11288                description: "".to_string(),
11289                last_modified: "".to_string(),
11290                architecture: "X86_64".to_string(),
11291            })
11292            .collect();
11293
11294        app.lambda_state.version_table.page_size = PageSize::Ten;
11295        app.lambda_state.version_table.selected = 0;
11296
11297        // Type "2" when focused on Pagination
11298        app.handle_action(Action::FilterInput('2'));
11299        assert_eq!(app.page_input, "2");
11300        assert_eq!(app.lambda_state.version_table.filter, ""); // Should not go to filter
11301
11302        // Press 'p' to go to page 2
11303        app.handle_action(Action::OpenColumnSelector);
11304        assert_eq!(app.lambda_state.version_table.selected, 10);
11305        assert_eq!(app.page_input, ""); // Should be cleared
11306    }
11307
11308    #[test]
11309    fn test_lambda_versions_filter_input() {
11310        use crate::lambda::Version;
11311
11312        let mut app = test_app();
11313        app.current_service = Service::LambdaFunctions;
11314        app.service_selected = true;
11315        app.lambda_state.current_function = Some("test-function".to_string());
11316        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11317        app.mode = Mode::FilterInput;
11318        app.lambda_state.version_input_focus = InputFocus::Filter;
11319
11320        // Add test versions
11321        app.lambda_state.version_table.items = vec![
11322            Version {
11323                version: "1".to_string(),
11324                aliases: "prod".to_string(),
11325                description: "Production".to_string(),
11326                last_modified: "".to_string(),
11327                architecture: "X86_64".to_string(),
11328            },
11329            Version {
11330                version: "2".to_string(),
11331                aliases: "staging".to_string(),
11332                description: "Staging".to_string(),
11333                last_modified: "".to_string(),
11334                architecture: "X86_64".to_string(),
11335            },
11336        ];
11337
11338        // Type filter text
11339        app.handle_action(Action::FilterInput('p'));
11340        app.handle_action(Action::FilterInput('r'));
11341        app.handle_action(Action::FilterInput('o'));
11342        app.handle_action(Action::FilterInput('d'));
11343        assert_eq!(app.lambda_state.version_table.filter, "prod");
11344
11345        // Backspace should work
11346        app.handle_action(Action::FilterBackspace);
11347        assert_eq!(app.lambda_state.version_table.filter, "pro");
11348    }
11349
11350    #[test]
11351    fn test_lambda_aliases_table_expansion() {
11352        use crate::lambda::Alias;
11353
11354        let mut app = test_app();
11355        app.current_service = Service::LambdaFunctions;
11356        app.service_selected = true;
11357        app.lambda_state.current_function = Some("test-function".to_string());
11358        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11359        app.mode = Mode::Normal;
11360
11361        app.lambda_state.alias_table.items = vec![
11362            Alias {
11363                name: "prod".to_string(),
11364                versions: "1".to_string(),
11365                description: "Production alias".to_string(),
11366            },
11367            Alias {
11368                name: "staging".to_string(),
11369                versions: "2".to_string(),
11370                description: "Staging alias".to_string(),
11371            },
11372        ];
11373
11374        app.lambda_state.alias_table.selected = 0;
11375
11376        // Select first alias - should open alias detail view (no tab change)
11377        app.handle_action(Action::Select);
11378        assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
11379        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11380
11381        // Go back
11382        app.handle_action(Action::GoBack);
11383        assert_eq!(app.lambda_state.current_alias, None);
11384        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11385
11386        // Select second alias
11387        app.lambda_state.alias_table.selected = 1;
11388        app.handle_action(Action::Select);
11389        assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
11390    }
11391
11392    #[test]
11393    fn test_lambda_versions_arrow_key_expansion() {
11394        use crate::lambda::Version;
11395
11396        let mut app = test_app();
11397        app.current_service = Service::LambdaFunctions;
11398        app.service_selected = true;
11399        app.lambda_state.current_function = Some("test-function".to_string());
11400        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11401        app.mode = Mode::Normal;
11402
11403        app.lambda_state.version_table.items = vec![Version {
11404            version: "1".to_string(),
11405            aliases: "prod".to_string(),
11406            description: "Production".to_string(),
11407            last_modified: "2024-01-01".to_string(),
11408            architecture: "X86_64".to_string(),
11409        }];
11410
11411        app.lambda_state.version_table.selected = 0;
11412
11413        // Right arrow expands
11414        app.handle_action(Action::NextPane);
11415        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11416
11417        // Left arrow collapses
11418        app.handle_action(Action::PrevPane);
11419        assert_eq!(app.lambda_state.version_table.expanded_item, None);
11420    }
11421
11422    #[test]
11423    fn test_lambda_version_detail_view() {
11424        use crate::lambda::Function;
11425
11426        let mut app = test_app();
11427        app.current_service = Service::LambdaFunctions;
11428        app.service_selected = true;
11429        app.lambda_state.current_function = Some("test-function".to_string());
11430        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11431        app.mode = Mode::Normal;
11432
11433        app.lambda_state.table.items = vec![Function {
11434            name: "test-function".to_string(),
11435            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11436            application: None,
11437            description: "Test".to_string(),
11438            package_type: "Zip".to_string(),
11439            runtime: "python3.12".to_string(),
11440            architecture: "X86_64".to_string(),
11441            code_size: 1024,
11442            code_sha256: "hash".to_string(),
11443            memory_mb: 128,
11444            timeout_seconds: 30,
11445            last_modified: "2024-01-01".to_string(),
11446            layers: vec![],
11447        }];
11448
11449        app.lambda_state.version_table.items = vec![crate::lambda::Version {
11450            version: "1".to_string(),
11451            aliases: "prod".to_string(),
11452            description: "Production".to_string(),
11453            last_modified: "2024-01-01".to_string(),
11454            architecture: "X86_64".to_string(),
11455        }];
11456
11457        app.lambda_state.version_table.selected = 0;
11458
11459        // Select version to open detail view
11460        app.handle_action(Action::Select);
11461        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11462        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11463
11464        // GoBack should go back to versions list
11465        app.handle_action(Action::GoBack);
11466        assert_eq!(app.lambda_state.current_version, None);
11467        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11468    }
11469
11470    #[test]
11471    fn test_lambda_version_detail_tabs() {
11472        use crate::lambda::Function;
11473
11474        let mut app = test_app();
11475        app.current_service = Service::LambdaFunctions;
11476        app.service_selected = true;
11477        app.lambda_state.current_function = Some("test-function".to_string());
11478        app.lambda_state.current_version = Some("1".to_string());
11479        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11480        app.mode = Mode::Normal;
11481
11482        app.lambda_state.table.items = vec![Function {
11483            name: "test-function".to_string(),
11484            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11485            application: None,
11486            description: "Test".to_string(),
11487            package_type: "Zip".to_string(),
11488            runtime: "python3.12".to_string(),
11489            architecture: "X86_64".to_string(),
11490            code_size: 1024,
11491            code_sha256: "hash".to_string(),
11492            memory_mb: 128,
11493            timeout_seconds: 30,
11494            last_modified: "2024-01-01".to_string(),
11495            layers: vec![],
11496        }];
11497
11498        // Tab should cycle between Code and Configuration only
11499        app.handle_action(Action::NextDetailTab);
11500        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11501
11502        app.handle_action(Action::NextDetailTab);
11503        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11504
11505        // BackTab should cycle backward
11506        app.handle_action(Action::PrevDetailTab);
11507        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11508    }
11509
11510    #[test]
11511    fn test_lambda_aliases_arrow_key_expansion() {
11512        use crate::lambda::Alias;
11513
11514        let mut app = test_app();
11515        app.current_service = Service::LambdaFunctions;
11516        app.service_selected = true;
11517        app.lambda_state.current_function = Some("test-function".to_string());
11518        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11519        app.mode = Mode::Normal;
11520
11521        app.lambda_state.alias_table.items = vec![Alias {
11522            name: "prod".to_string(),
11523            versions: "1".to_string(),
11524            description: "Production alias".to_string(),
11525        }];
11526
11527        app.lambda_state.alias_table.selected = 0;
11528
11529        // Right arrow expands
11530        app.handle_action(Action::NextPane);
11531        assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
11532
11533        // Left arrow collapses
11534        app.handle_action(Action::PrevPane);
11535        assert_eq!(app.lambda_state.alias_table.expanded_item, None);
11536    }
11537
11538    #[test]
11539    fn test_lambda_functions_arrow_key_expansion() {
11540        use crate::lambda::Function;
11541
11542        let mut app = test_app();
11543        app.current_service = Service::LambdaFunctions;
11544        app.service_selected = true;
11545        app.mode = Mode::Normal;
11546
11547        app.lambda_state.table.items = vec![Function {
11548            name: "test-function".to_string(),
11549            arn: "arn".to_string(),
11550            application: None,
11551            description: "Test".to_string(),
11552            package_type: "Zip".to_string(),
11553            runtime: "python3.12".to_string(),
11554            architecture: "X86_64".to_string(),
11555            code_size: 1024,
11556            code_sha256: "hash".to_string(),
11557            memory_mb: 128,
11558            timeout_seconds: 30,
11559            last_modified: "2024-01-01".to_string(),
11560            layers: vec![],
11561        }];
11562
11563        app.lambda_state.table.selected = 0;
11564
11565        // Right arrow expands
11566        app.handle_action(Action::NextPane);
11567        assert_eq!(app.lambda_state.table.expanded_item, Some(0));
11568
11569        // Left arrow collapses
11570        app.handle_action(Action::PrevPane);
11571        assert_eq!(app.lambda_state.table.expanded_item, None);
11572    }
11573
11574    #[test]
11575    fn test_lambda_version_detail_with_application() {
11576        use crate::lambda::Function;
11577
11578        let mut app = test_app();
11579        app.current_service = Service::LambdaFunctions;
11580        app.service_selected = true;
11581        app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
11582        app.lambda_state.current_version = Some("1".to_string());
11583        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11584        app.mode = Mode::Normal;
11585
11586        app.lambda_state.table.items = vec![Function {
11587            name: "storefront-studio-beta-api".to_string(),
11588            arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
11589                .to_string(),
11590            application: Some("storefront-studio-beta".to_string()),
11591            description: "API function".to_string(),
11592            package_type: "Zip".to_string(),
11593            runtime: "python3.12".to_string(),
11594            architecture: "X86_64".to_string(),
11595            code_size: 1024,
11596            code_sha256: "hash".to_string(),
11597            memory_mb: 128,
11598            timeout_seconds: 30,
11599            last_modified: "2024-01-01".to_string(),
11600            layers: vec![],
11601        }];
11602
11603        // Verify function has application extracted
11604        assert_eq!(
11605            app.lambda_state.table.items[0].application,
11606            Some("storefront-studio-beta".to_string())
11607        );
11608        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11609    }
11610
11611    #[test]
11612    fn test_lambda_layer_navigation() {
11613        use crate::lambda::{Function, Layer};
11614
11615        let mut app = test_app();
11616        app.current_service = Service::LambdaFunctions;
11617        app.service_selected = true;
11618        app.lambda_state.current_function = Some("test-function".to_string());
11619        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11620        app.mode = Mode::Normal;
11621
11622        app.lambda_state.table.items = vec![Function {
11623            name: "test-function".to_string(),
11624            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11625            application: None,
11626            description: "Test".to_string(),
11627            package_type: "Zip".to_string(),
11628            runtime: "python3.12".to_string(),
11629            architecture: "X86_64".to_string(),
11630            code_size: 1024,
11631            code_sha256: "hash".to_string(),
11632            memory_mb: 128,
11633            timeout_seconds: 30,
11634            last_modified: "2024-01-01".to_string(),
11635            layers: vec![
11636                Layer {
11637                    merge_order: "1".to_string(),
11638                    name: "layer1".to_string(),
11639                    layer_version: "1".to_string(),
11640                    compatible_runtimes: "python3.9".to_string(),
11641                    compatible_architectures: "x86_64".to_string(),
11642                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11643                },
11644                Layer {
11645                    merge_order: "2".to_string(),
11646                    name: "layer2".to_string(),
11647                    layer_version: "2".to_string(),
11648                    compatible_runtimes: "python3.9".to_string(),
11649                    compatible_architectures: "x86_64".to_string(),
11650                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11651                },
11652                Layer {
11653                    merge_order: "3".to_string(),
11654                    name: "layer3".to_string(),
11655                    layer_version: "3".to_string(),
11656                    compatible_runtimes: "python3.9".to_string(),
11657                    compatible_architectures: "x86_64".to_string(),
11658                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
11659                },
11660            ],
11661        }];
11662
11663        assert_eq!(app.lambda_state.layer_selected, 0);
11664
11665        app.handle_action(Action::NextItem);
11666        assert_eq!(app.lambda_state.layer_selected, 1);
11667
11668        app.handle_action(Action::NextItem);
11669        assert_eq!(app.lambda_state.layer_selected, 2);
11670
11671        app.handle_action(Action::NextItem);
11672        assert_eq!(app.lambda_state.layer_selected, 2);
11673
11674        app.handle_action(Action::PrevItem);
11675        assert_eq!(app.lambda_state.layer_selected, 1);
11676
11677        app.handle_action(Action::PrevItem);
11678        assert_eq!(app.lambda_state.layer_selected, 0);
11679
11680        app.handle_action(Action::PrevItem);
11681        assert_eq!(app.lambda_state.layer_selected, 0);
11682    }
11683
11684    #[test]
11685    fn test_lambda_layer_expansion() {
11686        use crate::lambda::{Function, Layer};
11687
11688        let mut app = test_app();
11689        app.current_service = Service::LambdaFunctions;
11690        app.service_selected = true;
11691        app.lambda_state.current_function = Some("test-function".to_string());
11692        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11693        app.mode = Mode::Normal;
11694
11695        app.lambda_state.table.items = vec![Function {
11696            name: "test-function".to_string(),
11697            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11698            application: None,
11699            description: "Test".to_string(),
11700            package_type: "Zip".to_string(),
11701            runtime: "python3.12".to_string(),
11702            architecture: "X86_64".to_string(),
11703            code_size: 1024,
11704            code_sha256: "hash".to_string(),
11705            memory_mb: 128,
11706            timeout_seconds: 30,
11707            last_modified: "2024-01-01".to_string(),
11708            layers: vec![Layer {
11709                merge_order: "1".to_string(),
11710                name: "test-layer".to_string(),
11711                layer_version: "1".to_string(),
11712                compatible_runtimes: "python3.9".to_string(),
11713                compatible_architectures: "x86_64".to_string(),
11714                version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
11715            }],
11716        }];
11717
11718        assert_eq!(app.lambda_state.layer_expanded, None);
11719
11720        app.handle_action(Action::NextPane);
11721        assert_eq!(app.lambda_state.layer_expanded, Some(0));
11722
11723        app.handle_action(Action::PrevPane);
11724        assert_eq!(app.lambda_state.layer_expanded, None);
11725
11726        app.handle_action(Action::NextPane);
11727        assert_eq!(app.lambda_state.layer_expanded, Some(0));
11728
11729        app.handle_action(Action::NextPane);
11730        assert_eq!(app.lambda_state.layer_expanded, None);
11731    }
11732
11733    #[test]
11734    fn test_lambda_layer_selection_and_expansion_workflow() {
11735        use crate::lambda::{Function, Layer};
11736
11737        let mut app = test_app();
11738        app.current_service = Service::LambdaFunctions;
11739        app.service_selected = true;
11740        app.lambda_state.current_function = Some("test-function".to_string());
11741        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11742        app.mode = Mode::Normal;
11743
11744        app.lambda_state.table.items = vec![Function {
11745            name: "test-function".to_string(),
11746            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11747            application: None,
11748            description: "Test".to_string(),
11749            package_type: "Zip".to_string(),
11750            runtime: "python3.12".to_string(),
11751            architecture: "X86_64".to_string(),
11752            code_size: 1024,
11753            code_sha256: "hash".to_string(),
11754            memory_mb: 128,
11755            timeout_seconds: 30,
11756            last_modified: "2024-01-01".to_string(),
11757            layers: vec![
11758                Layer {
11759                    merge_order: "1".to_string(),
11760                    name: "layer1".to_string(),
11761                    layer_version: "1".to_string(),
11762                    compatible_runtimes: "python3.9".to_string(),
11763                    compatible_architectures: "x86_64".to_string(),
11764                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11765                },
11766                Layer {
11767                    merge_order: "2".to_string(),
11768                    name: "layer2".to_string(),
11769                    layer_version: "2".to_string(),
11770                    compatible_runtimes: "python3.9".to_string(),
11771                    compatible_architectures: "x86_64".to_string(),
11772                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11773                },
11774            ],
11775        }];
11776
11777        // Start at layer 0
11778        assert_eq!(app.lambda_state.layer_selected, 0);
11779        assert_eq!(app.lambda_state.layer_expanded, None);
11780
11781        // Expand layer 0
11782        app.handle_action(Action::NextPane);
11783        assert_eq!(app.lambda_state.layer_selected, 0);
11784        assert_eq!(app.lambda_state.layer_expanded, Some(0));
11785
11786        // Navigate to layer 1 while layer 0 is expanded
11787        app.handle_action(Action::NextItem);
11788        assert_eq!(app.lambda_state.layer_selected, 1);
11789        assert_eq!(app.lambda_state.layer_expanded, Some(0)); // Still expanded
11790
11791        // Expand layer 1 (should collapse layer 0 and expand layer 1)
11792        app.handle_action(Action::NextPane);
11793        assert_eq!(app.lambda_state.layer_selected, 1);
11794        assert_eq!(app.lambda_state.layer_expanded, Some(1));
11795
11796        // Collapse layer 1
11797        app.handle_action(Action::PrevPane);
11798        assert_eq!(app.lambda_state.layer_selected, 1);
11799        assert_eq!(app.lambda_state.layer_expanded, None);
11800
11801        // Navigate back to layer 0
11802        app.handle_action(Action::PrevItem);
11803        assert_eq!(app.lambda_state.layer_selected, 0);
11804        assert_eq!(app.lambda_state.layer_expanded, None);
11805    }
11806
11807    #[test]
11808    fn test_backtab_cycles_detail_tabs_backward() {
11809        let mut app = test_app();
11810        app.mode = Mode::Normal;
11811
11812        // Test Lambda detail tabs
11813        app.current_service = Service::LambdaFunctions;
11814        app.service_selected = true;
11815        app.lambda_state.current_function = Some("test-function".to_string());
11816        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11817
11818        app.handle_action(Action::PrevDetailTab);
11819        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11820
11821        app.handle_action(Action::PrevDetailTab);
11822        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11823
11824        // Test IAM Roles detail tabs
11825        app.current_service = Service::IamRoles;
11826        app.iam_state.current_role = Some("test-role".to_string());
11827        app.iam_state.role_tab = RoleTab::Permissions;
11828
11829        app.handle_action(Action::PrevDetailTab);
11830        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
11831
11832        // Test IAM Users detail tabs
11833        app.current_service = Service::IamUsers;
11834        app.iam_state.current_user = Some("test-user".to_string());
11835        app.iam_state.user_tab = UserTab::Permissions;
11836
11837        app.handle_action(Action::PrevDetailTab);
11838        assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
11839
11840        // Test IAM Groups detail tabs
11841        app.current_service = Service::IamUserGroups;
11842        app.iam_state.current_group = Some("test-group".to_string());
11843        app.iam_state.group_tab = GroupTab::Permissions;
11844
11845        app.handle_action(Action::PrevDetailTab);
11846        assert_eq!(app.iam_state.group_tab, GroupTab::Users);
11847
11848        // Test S3 object tabs
11849        app.current_service = Service::S3Buckets;
11850        app.s3_state.current_bucket = Some("test-bucket".to_string());
11851        app.s3_state.object_tab = S3ObjectTab::Properties;
11852
11853        app.handle_action(Action::PrevDetailTab);
11854        assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
11855
11856        // Test ECR repository tabs (Private/Public)
11857        app.current_service = Service::EcrRepositories;
11858        app.ecr_state.current_repository = None;
11859        app.ecr_state.tab = EcrTab::Private;
11860
11861        app.handle_action(Action::PrevDetailTab);
11862        assert_eq!(app.ecr_state.tab, EcrTab::Public);
11863
11864        // Test CloudFormation detail tabs
11865        app.current_service = Service::CloudFormationStacks;
11866        app.cfn_state.current_stack = Some("test-stack".to_string());
11867        app.cfn_state.detail_tab = CfnDetailTab::Resources;
11868    }
11869
11870    #[test]
11871    fn test_cloudformation_status_filter_active() {
11872        use crate::ui::cfn::StatusFilter;
11873        let filter = StatusFilter::Active;
11874        assert!(filter.matches("CREATE_IN_PROGRESS"));
11875        assert!(filter.matches("UPDATE_IN_PROGRESS"));
11876        assert!(!filter.matches("CREATE_COMPLETE"));
11877        assert!(!filter.matches("DELETE_COMPLETE"));
11878        assert!(!filter.matches("CREATE_FAILED"));
11879    }
11880
11881    #[test]
11882    fn test_cloudformation_status_filter_complete() {
11883        use crate::ui::cfn::StatusFilter;
11884        let filter = StatusFilter::Complete;
11885        assert!(filter.matches("CREATE_COMPLETE"));
11886        assert!(filter.matches("UPDATE_COMPLETE"));
11887        assert!(!filter.matches("DELETE_COMPLETE"));
11888        assert!(!filter.matches("CREATE_IN_PROGRESS"));
11889    }
11890
11891    #[test]
11892    fn test_cloudformation_status_filter_failed() {
11893        use crate::ui::cfn::StatusFilter;
11894        let filter = StatusFilter::Failed;
11895        assert!(filter.matches("CREATE_FAILED"));
11896        assert!(filter.matches("UPDATE_FAILED"));
11897        assert!(!filter.matches("CREATE_COMPLETE"));
11898    }
11899
11900    #[test]
11901    fn test_cloudformation_status_filter_deleted() {
11902        use crate::ui::cfn::StatusFilter;
11903        let filter = StatusFilter::Deleted;
11904        assert!(filter.matches("DELETE_COMPLETE"));
11905        assert!(filter.matches("DELETE_IN_PROGRESS"));
11906        assert!(!filter.matches("CREATE_COMPLETE"));
11907    }
11908
11909    #[test]
11910    fn test_cloudformation_status_filter_in_progress() {
11911        use crate::ui::cfn::StatusFilter;
11912        let filter = StatusFilter::InProgress;
11913        assert!(filter.matches("CREATE_IN_PROGRESS"));
11914        assert!(filter.matches("UPDATE_IN_PROGRESS"));
11915        assert!(filter.matches("DELETE_IN_PROGRESS"));
11916        assert!(!filter.matches("CREATE_COMPLETE"));
11917    }
11918
11919    #[test]
11920    fn test_cloudformation_status_filter_cycle() {
11921        use crate::ui::cfn::StatusFilter;
11922        let filter = StatusFilter::All;
11923        assert_eq!(filter.next(), StatusFilter::Active);
11924        assert_eq!(filter.next().next(), StatusFilter::Complete);
11925        assert_eq!(filter.next().next().next(), StatusFilter::Failed);
11926        assert_eq!(filter.next().next().next().next(), StatusFilter::Deleted);
11927        assert_eq!(
11928            filter.next().next().next().next().next(),
11929            StatusFilter::InProgress
11930        );
11931        assert_eq!(
11932            filter.next().next().next().next().next().next(),
11933            StatusFilter::All
11934        );
11935    }
11936
11937    #[test]
11938    fn test_cloudformation_default_columns() {
11939        let app = test_app();
11940        assert_eq!(app.cfn_visible_column_ids.len(), 4);
11941        assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
11942        assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
11943        assert!(app
11944            .cfn_visible_column_ids
11945            .contains(&CfnColumn::CreatedTime.id()));
11946        assert!(app
11947            .cfn_visible_column_ids
11948            .contains(&CfnColumn::Description.id()));
11949    }
11950
11951    #[test]
11952    fn test_cloudformation_all_columns() {
11953        let app = test_app();
11954        assert_eq!(app.cfn_column_ids.len(), 10);
11955    }
11956
11957    #[test]
11958    fn test_cloudformation_filter_by_name() {
11959        use crate::ui::cfn::StatusFilter;
11960        let mut app = test_app();
11961        app.cfn_state.status_filter = StatusFilter::Complete;
11962        app.cfn_state.table.items = vec![
11963            CfnStack {
11964                name: "my-stack".to_string(),
11965                stack_id: "id1".to_string(),
11966                status: "CREATE_COMPLETE".to_string(),
11967                created_time: "2024-01-01".to_string(),
11968                updated_time: String::new(),
11969                deleted_time: String::new(),
11970                drift_status: String::new(),
11971                last_drift_check_time: String::new(),
11972                status_reason: String::new(),
11973                description: String::new(),
11974                detailed_status: String::new(),
11975                root_stack: String::new(),
11976                parent_stack: String::new(),
11977                termination_protection: false,
11978                iam_role: String::new(),
11979                tags: Vec::new(),
11980                stack_policy: String::new(),
11981                rollback_monitoring_time: String::new(),
11982                rollback_alarms: Vec::new(),
11983                notification_arns: Vec::new(),
11984            },
11985            CfnStack {
11986                name: "other-stack".to_string(),
11987                stack_id: "id2".to_string(),
11988                status: "CREATE_COMPLETE".to_string(),
11989                created_time: "2024-01-02".to_string(),
11990                updated_time: String::new(),
11991                deleted_time: String::new(),
11992                drift_status: String::new(),
11993                last_drift_check_time: String::new(),
11994                status_reason: String::new(),
11995                description: String::new(),
11996                detailed_status: String::new(),
11997                root_stack: String::new(),
11998                parent_stack: String::new(),
11999                termination_protection: false,
12000                iam_role: String::new(),
12001                tags: Vec::new(),
12002                stack_policy: String::new(),
12003                rollback_monitoring_time: String::new(),
12004                rollback_alarms: Vec::new(),
12005                notification_arns: Vec::new(),
12006            },
12007        ];
12008
12009        app.cfn_state.table.filter = "my".to_string();
12010        let filtered = app.filtered_cloudformation_stacks();
12011        assert_eq!(filtered.len(), 1);
12012        assert_eq!(filtered[0].name, "my-stack");
12013    }
12014
12015    #[test]
12016    fn test_cloudformation_filter_by_description() {
12017        use crate::ui::cfn::StatusFilter;
12018        let mut app = test_app();
12019        app.cfn_state.status_filter = StatusFilter::Complete;
12020        app.cfn_state.table.items = vec![CfnStack {
12021            name: "stack1".to_string(),
12022            stack_id: "id1".to_string(),
12023            status: "CREATE_COMPLETE".to_string(),
12024            created_time: "2024-01-01".to_string(),
12025            updated_time: String::new(),
12026            deleted_time: String::new(),
12027            drift_status: String::new(),
12028            last_drift_check_time: String::new(),
12029            status_reason: String::new(),
12030            description: "production stack".to_string(),
12031            detailed_status: String::new(),
12032            root_stack: String::new(),
12033            parent_stack: String::new(),
12034            termination_protection: false,
12035            iam_role: String::new(),
12036            tags: Vec::new(),
12037            stack_policy: String::new(),
12038            rollback_monitoring_time: String::new(),
12039            rollback_alarms: Vec::new(),
12040            notification_arns: Vec::new(),
12041        }];
12042
12043        app.cfn_state.table.filter = "production".to_string();
12044        let filtered = app.filtered_cloudformation_stacks();
12045        assert_eq!(filtered.len(), 1);
12046    }
12047
12048    #[test]
12049    fn test_cloudformation_status_filter_applied() {
12050        use crate::ui::cfn::StatusFilter;
12051        let mut app = test_app();
12052        app.cfn_state.table.items = vec![
12053            CfnStack {
12054                name: "complete-stack".to_string(),
12055                stack_id: "id1".to_string(),
12056                status: "CREATE_COMPLETE".to_string(),
12057                created_time: "2024-01-01".to_string(),
12058                updated_time: String::new(),
12059                deleted_time: String::new(),
12060                drift_status: String::new(),
12061                last_drift_check_time: String::new(),
12062                status_reason: String::new(),
12063                description: String::new(),
12064                detailed_status: String::new(),
12065                root_stack: String::new(),
12066                parent_stack: String::new(),
12067                termination_protection: false,
12068                iam_role: String::new(),
12069                tags: Vec::new(),
12070                stack_policy: String::new(),
12071                rollback_monitoring_time: String::new(),
12072                rollback_alarms: Vec::new(),
12073                notification_arns: Vec::new(),
12074            },
12075            CfnStack {
12076                name: "failed-stack".to_string(),
12077                stack_id: "id2".to_string(),
12078                status: "CREATE_FAILED".to_string(),
12079                created_time: "2024-01-02".to_string(),
12080                updated_time: String::new(),
12081                deleted_time: String::new(),
12082                drift_status: String::new(),
12083                last_drift_check_time: String::new(),
12084                status_reason: String::new(),
12085                description: String::new(),
12086                detailed_status: String::new(),
12087                root_stack: String::new(),
12088                parent_stack: String::new(),
12089                termination_protection: false,
12090                iam_role: String::new(),
12091                tags: Vec::new(),
12092                stack_policy: String::new(),
12093                rollback_monitoring_time: String::new(),
12094                rollback_alarms: Vec::new(),
12095                notification_arns: Vec::new(),
12096            },
12097        ];
12098
12099        app.cfn_state.status_filter = StatusFilter::Complete;
12100        let filtered = app.filtered_cloudformation_stacks();
12101        assert_eq!(filtered.len(), 1);
12102        assert_eq!(filtered[0].name, "complete-stack");
12103
12104        app.cfn_state.status_filter = StatusFilter::Failed;
12105        let filtered = app.filtered_cloudformation_stacks();
12106        assert_eq!(filtered.len(), 1);
12107        assert_eq!(filtered[0].name, "failed-stack");
12108    }
12109
12110    #[test]
12111    fn test_cloudformation_default_page_size() {
12112        let app = test_app();
12113        assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
12114    }
12115
12116    #[test]
12117    fn test_cloudformation_default_status_filter() {
12118        use crate::ui::cfn::StatusFilter;
12119        let app = test_app();
12120        assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
12121    }
12122
12123    #[test]
12124    fn test_cloudformation_view_nested_default_false() {
12125        let app = test_app();
12126        assert!(!app.cfn_state.view_nested);
12127    }
12128
12129    #[test]
12130    fn test_cloudformation_pagination_hotkeys() {
12131        use crate::ui::cfn::StatusFilter;
12132        let mut app = test_app();
12133        app.current_service = Service::CloudFormationStacks;
12134        app.service_selected = true;
12135        app.cfn_state.status_filter = StatusFilter::All;
12136
12137        // Add 150 stacks
12138        for i in 0..150 {
12139            app.cfn_state.table.items.push(CfnStack {
12140                name: format!("stack-{}", i),
12141                stack_id: format!("id-{}", i),
12142                status: "CREATE_COMPLETE".to_string(),
12143                created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12144                updated_time: String::new(),
12145                deleted_time: String::new(),
12146                drift_status: String::new(),
12147                last_drift_check_time: String::new(),
12148                status_reason: String::new(),
12149                description: String::new(),
12150                detailed_status: String::new(),
12151                root_stack: String::new(),
12152                parent_stack: String::new(),
12153                termination_protection: false,
12154                iam_role: String::new(),
12155                tags: vec![],
12156                stack_policy: String::new(),
12157                rollback_monitoring_time: String::new(),
12158                rollback_alarms: vec![],
12159                notification_arns: vec![],
12160            });
12161        }
12162
12163        // Go to page 2
12164        app.go_to_page(2);
12165        assert_eq!(app.cfn_state.table.selected, 50);
12166
12167        // Go to page 3
12168        app.go_to_page(3);
12169        assert_eq!(app.cfn_state.table.selected, 100);
12170
12171        // Go to page 1
12172        app.go_to_page(1);
12173        assert_eq!(app.cfn_state.table.selected, 0);
12174    }
12175
12176    #[test]
12177    fn test_cloudformation_tab_cycling_in_filter_mode() {
12178        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12179        let mut app = test_app();
12180        app.current_service = Service::CloudFormationStacks;
12181        app.service_selected = true;
12182        app.mode = Mode::FilterInput;
12183        app.cfn_state.input_focus = InputFocus::Filter;
12184
12185        // Tab to StatusFilter
12186        app.handle_action(Action::NextFilterFocus);
12187        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12188
12189        // Tab to ViewNested
12190        app.handle_action(Action::NextFilterFocus);
12191        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12192
12193        // Tab to Pagination
12194        app.handle_action(Action::NextFilterFocus);
12195        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12196
12197        // Tab back to Filter
12198        app.handle_action(Action::NextFilterFocus);
12199        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12200    }
12201
12202    #[test]
12203    fn test_cloudformation_timestamp_format_includes_utc() {
12204        let stack = CfnStack {
12205            name: "test-stack".to_string(),
12206            stack_id: "id-123".to_string(),
12207            status: "CREATE_COMPLETE".to_string(),
12208            created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
12209            updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
12210            deleted_time: String::new(),
12211            drift_status: String::new(),
12212            last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
12213            status_reason: String::new(),
12214            description: String::new(),
12215            detailed_status: String::new(),
12216            root_stack: String::new(),
12217            parent_stack: String::new(),
12218            termination_protection: false,
12219            iam_role: String::new(),
12220            tags: vec![],
12221            stack_policy: String::new(),
12222            rollback_monitoring_time: String::new(),
12223            rollback_alarms: vec![],
12224            notification_arns: vec![],
12225        };
12226
12227        assert!(stack.created_time.contains("(UTC)"));
12228        assert!(stack.updated_time.contains("(UTC)"));
12229        assert!(stack.last_drift_check_time.contains("(UTC)"));
12230        assert_eq!(stack.created_time.len(), 25);
12231    }
12232
12233    #[test]
12234    fn test_cloudformation_enter_drills_into_stack_view() {
12235        use crate::ui::cfn::StatusFilter;
12236        let mut app = test_app();
12237        app.current_service = Service::CloudFormationStacks;
12238        app.service_selected = true;
12239        app.mode = Mode::Normal;
12240        app.cfn_state.status_filter = StatusFilter::All;
12241        app.tabs = vec![Tab {
12242            service: Service::CloudFormationStacks,
12243            title: "CloudFormation > Stacks".to_string(),
12244            breadcrumb: "CloudFormation > Stacks".to_string(),
12245        }];
12246        app.current_tab = 0;
12247
12248        app.cfn_state.table.items.push(CfnStack {
12249            name: "test-stack".to_string(),
12250            stack_id: "id-123".to_string(),
12251            status: "CREATE_COMPLETE".to_string(),
12252            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12253            updated_time: String::new(),
12254            deleted_time: String::new(),
12255            drift_status: String::new(),
12256            last_drift_check_time: String::new(),
12257            status_reason: String::new(),
12258            description: String::new(),
12259            detailed_status: String::new(),
12260            root_stack: String::new(),
12261            parent_stack: String::new(),
12262            termination_protection: false,
12263            iam_role: String::new(),
12264            tags: vec![],
12265            stack_policy: String::new(),
12266            rollback_monitoring_time: String::new(),
12267            rollback_alarms: vec![],
12268            notification_arns: vec![],
12269        });
12270
12271        app.cfn_state.table.reset();
12272        assert_eq!(app.cfn_state.current_stack, None);
12273
12274        // Press Enter - should drill into stack detail view
12275        app.handle_action(Action::Select);
12276        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
12277    }
12278
12279    #[test]
12280    fn test_cloudformation_arrow_keys_expand_collapse() {
12281        use crate::ui::cfn::StatusFilter;
12282        let mut app = test_app();
12283        app.current_service = Service::CloudFormationStacks;
12284        app.service_selected = true;
12285        app.mode = Mode::Normal;
12286        app.cfn_state.status_filter = StatusFilter::All;
12287
12288        app.cfn_state.table.items.push(CfnStack {
12289            name: "test-stack".to_string(),
12290            stack_id: "id-123".to_string(),
12291            status: "CREATE_COMPLETE".to_string(),
12292            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12293            updated_time: String::new(),
12294            deleted_time: String::new(),
12295            drift_status: String::new(),
12296            last_drift_check_time: String::new(),
12297            status_reason: String::new(),
12298            description: String::new(),
12299            detailed_status: String::new(),
12300            root_stack: String::new(),
12301            parent_stack: String::new(),
12302            termination_protection: false,
12303            iam_role: String::new(),
12304            tags: vec![],
12305            stack_policy: String::new(),
12306            rollback_monitoring_time: String::new(),
12307            rollback_alarms: vec![],
12308            notification_arns: vec![],
12309        });
12310
12311        app.cfn_state.table.reset();
12312        assert_eq!(app.cfn_state.table.expanded_item, None);
12313
12314        // Right arrow - should expand
12315        app.handle_action(Action::NextPane);
12316        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
12317
12318        // Left arrow - should collapse
12319        app.handle_action(Action::PrevPane);
12320        assert_eq!(app.cfn_state.table.expanded_item, None);
12321
12322        // Verify current_stack is still None (not drilled in)
12323        assert_eq!(app.cfn_state.current_stack, None);
12324    }
12325
12326    #[test]
12327    fn test_cloudformation_tab_cycling() {
12328        use crate::ui::cfn::{DetailTab, StatusFilter};
12329        let mut app = test_app();
12330        app.current_service = Service::CloudFormationStacks;
12331        app.service_selected = true;
12332        app.mode = Mode::Normal;
12333        app.cfn_state.status_filter = StatusFilter::All;
12334        app.cfn_state.current_stack = Some("test-stack".to_string());
12335
12336        assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
12337    }
12338
12339    #[test]
12340    fn test_cloudformation_console_url() {
12341        use crate::ui::cfn::{DetailTab, StatusFilter};
12342        let mut app = test_app();
12343        app.current_service = Service::CloudFormationStacks;
12344        app.service_selected = true;
12345        app.cfn_state.status_filter = StatusFilter::All;
12346
12347        app.cfn_state.table.items.push(CfnStack {
12348            name: "test-stack".to_string(),
12349            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12350                .to_string(),
12351            status: "CREATE_COMPLETE".to_string(),
12352            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12353            updated_time: String::new(),
12354            deleted_time: String::new(),
12355            drift_status: String::new(),
12356            last_drift_check_time: String::new(),
12357            status_reason: String::new(),
12358            description: String::new(),
12359            detailed_status: String::new(),
12360            root_stack: String::new(),
12361            parent_stack: String::new(),
12362            termination_protection: false,
12363            iam_role: String::new(),
12364            tags: vec![],
12365            stack_policy: String::new(),
12366            rollback_monitoring_time: String::new(),
12367            rollback_alarms: vec![],
12368            notification_arns: vec![],
12369        });
12370
12371        app.cfn_state.current_stack = Some("test-stack".to_string());
12372
12373        // Stack info URL
12374        app.cfn_state.detail_tab = DetailTab::StackInfo;
12375        let url = app.get_console_url();
12376        assert!(url.contains("stackinfo"));
12377        assert!(url.contains("arn%3Aaws%3Acloudformation"));
12378
12379        // Events URL
12380        app.cfn_state.detail_tab = DetailTab::Events;
12381        let url = app.get_console_url();
12382        assert!(url.contains("events"));
12383        assert!(url.contains("arn%3Aaws%3Acloudformation"));
12384    }
12385
12386    #[test]
12387    fn test_iam_role_select() {
12388        let mut app = test_app();
12389        app.current_service = Service::IamRoles;
12390        app.service_selected = true;
12391        app.mode = Mode::Normal;
12392
12393        app.iam_state.roles.items = vec![
12394            crate::iam::IamRole {
12395                role_name: "role1".to_string(),
12396                path: "/".to_string(),
12397                trusted_entities: "AWS Service: ec2".to_string(),
12398                last_activity: "-".to_string(),
12399                arn: "arn:aws:iam::123456789012:role/role1".to_string(),
12400                creation_time: "2025-01-01".to_string(),
12401                description: "Test role 1".to_string(),
12402                max_session_duration: Some(3600),
12403            },
12404            crate::iam::IamRole {
12405                role_name: "role2".to_string(),
12406                path: "/".to_string(),
12407                trusted_entities: "AWS Service: lambda".to_string(),
12408                last_activity: "-".to_string(),
12409                arn: "arn:aws:iam::123456789012:role/role2".to_string(),
12410                creation_time: "2025-01-02".to_string(),
12411                description: "Test role 2".to_string(),
12412                max_session_duration: Some(7200),
12413            },
12414        ];
12415
12416        // Select first role
12417        app.iam_state.roles.selected = 0;
12418        app.handle_action(Action::Select);
12419
12420        assert_eq!(
12421            app.iam_state.current_role,
12422            Some("role1".to_string()),
12423            "Should open role detail view"
12424        );
12425        assert_eq!(
12426            app.iam_state.role_tab,
12427            RoleTab::Permissions,
12428            "Should default to Permissions tab"
12429        );
12430    }
12431
12432    #[test]
12433    fn test_iam_role_back_navigation() {
12434        let mut app = test_app();
12435        app.current_service = Service::IamRoles;
12436        app.service_selected = true;
12437        app.iam_state.current_role = Some("test-role".to_string());
12438
12439        app.handle_action(Action::GoBack);
12440
12441        assert_eq!(
12442            app.iam_state.current_role, None,
12443            "Should return to roles list"
12444        );
12445    }
12446
12447    #[test]
12448    fn test_iam_role_tab_navigation() {
12449        let mut app = test_app();
12450        app.current_service = Service::IamRoles;
12451        app.service_selected = true;
12452        app.iam_state.current_role = Some("test-role".to_string());
12453        app.iam_state.role_tab = RoleTab::Permissions;
12454
12455        app.handle_action(Action::NextDetailTab);
12456
12457        assert_eq!(
12458            app.iam_state.role_tab,
12459            RoleTab::TrustRelationships,
12460            "Should move to next tab"
12461        );
12462    }
12463
12464    #[test]
12465    fn test_iam_role_tab_cycle_order() {
12466        let mut app = test_app();
12467        app.current_service = Service::IamRoles;
12468        app.service_selected = true;
12469        app.iam_state.current_role = Some("test-role".to_string());
12470        app.iam_state.role_tab = RoleTab::Permissions;
12471
12472        app.handle_action(Action::NextDetailTab);
12473        assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
12474
12475        app.handle_action(Action::NextDetailTab);
12476        assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
12477
12478        app.handle_action(Action::NextDetailTab);
12479        assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
12480
12481        app.handle_action(Action::NextDetailTab);
12482        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
12483
12484        app.handle_action(Action::NextDetailTab);
12485        assert_eq!(
12486            app.iam_state.role_tab,
12487            RoleTab::Permissions,
12488            "Should cycle back to first tab"
12489        );
12490    }
12491
12492    #[test]
12493    fn test_iam_role_pagination() {
12494        let mut app = test_app();
12495        app.current_service = Service::IamRoles;
12496        app.service_selected = true;
12497        app.iam_state.roles.page_size = crate::common::PageSize::Ten;
12498
12499        app.iam_state.roles.items = (0..25)
12500            .map(|i| crate::iam::IamRole {
12501                role_name: format!("role{}", i),
12502                path: "/".to_string(),
12503                trusted_entities: "AWS Service: ec2".to_string(),
12504                last_activity: "-".to_string(),
12505                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12506                creation_time: "2025-01-01".to_string(),
12507                description: format!("Test role {}", i),
12508                max_session_duration: Some(3600),
12509            })
12510            .collect();
12511
12512        // Jump to page 2
12513        app.go_to_page(2);
12514
12515        assert_eq!(
12516            app.iam_state.roles.selected, 10,
12517            "Should select first item of page 2"
12518        );
12519        assert_eq!(
12520            app.iam_state.roles.scroll_offset, 10,
12521            "Should update scroll offset"
12522        );
12523    }
12524
12525    #[test]
12526    fn test_tags_table_populated_on_role_detail() {
12527        let mut app = test_app();
12528        app.current_service = Service::IamRoles;
12529        app.service_selected = true;
12530        app.mode = Mode::Normal;
12531        app.iam_state.roles.items = vec![crate::iam::IamRole {
12532            role_name: "TestRole".to_string(),
12533            path: "/".to_string(),
12534            trusted_entities: String::new(),
12535            last_activity: String::new(),
12536            arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
12537            creation_time: "2025-01-01".to_string(),
12538            description: String::new(),
12539            max_session_duration: Some(3600),
12540        }];
12541
12542        // Manually populate tags to test table rendering
12543        app.iam_state.tags.items = vec![
12544            crate::iam::RoleTag {
12545                key: "Environment".to_string(),
12546                value: "Production".to_string(),
12547            },
12548            crate::iam::RoleTag {
12549                key: "Team".to_string(),
12550                value: "Platform".to_string(),
12551            },
12552        ];
12553
12554        assert_eq!(app.iam_state.tags.items.len(), 2);
12555        assert_eq!(app.iam_state.tags.items[0].key, "Environment");
12556        assert_eq!(app.iam_state.tags.items[0].value, "Production");
12557        assert_eq!(app.iam_state.tags.selected, 0);
12558    }
12559
12560    #[test]
12561    fn test_tags_table_navigation() {
12562        let mut app = test_app();
12563        app.current_service = Service::IamRoles;
12564        app.service_selected = true;
12565        app.mode = Mode::Normal;
12566        app.iam_state.current_role = Some("TestRole".to_string());
12567        app.iam_state.role_tab = RoleTab::Tags;
12568        app.iam_state.tags.items = vec![
12569            crate::iam::RoleTag {
12570                key: "Tag1".to_string(),
12571                value: "Value1".to_string(),
12572            },
12573            crate::iam::RoleTag {
12574                key: "Tag2".to_string(),
12575                value: "Value2".to_string(),
12576            },
12577        ];
12578
12579        app.handle_action(Action::NextItem);
12580        assert_eq!(app.iam_state.tags.selected, 1);
12581
12582        app.handle_action(Action::PrevItem);
12583        assert_eq!(app.iam_state.tags.selected, 0);
12584    }
12585
12586    #[test]
12587    fn test_last_accessed_table_navigation() {
12588        let mut app = test_app();
12589        app.current_service = Service::IamRoles;
12590        app.service_selected = true;
12591        app.mode = Mode::Normal;
12592        app.iam_state.current_role = Some("TestRole".to_string());
12593        app.iam_state.role_tab = RoleTab::LastAccessed;
12594        app.iam_state.last_accessed_services.items = vec![
12595            crate::iam::LastAccessedService {
12596                service: "S3".to_string(),
12597                policies_granting: "Policy1".to_string(),
12598                last_accessed: "2025-01-01".to_string(),
12599            },
12600            crate::iam::LastAccessedService {
12601                service: "EC2".to_string(),
12602                policies_granting: "Policy2".to_string(),
12603                last_accessed: "2025-01-02".to_string(),
12604            },
12605        ];
12606
12607        app.handle_action(Action::NextItem);
12608        assert_eq!(app.iam_state.last_accessed_services.selected, 1);
12609
12610        app.handle_action(Action::PrevItem);
12611        assert_eq!(app.iam_state.last_accessed_services.selected, 0);
12612    }
12613
12614    #[test]
12615    fn test_cfn_input_focus_next() {
12616        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12617        let mut app = test_app();
12618        app.current_service = Service::CloudFormationStacks;
12619        app.mode = Mode::FilterInput;
12620        app.cfn_state.input_focus = InputFocus::Filter;
12621
12622        app.handle_action(Action::NextFilterFocus);
12623        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12624
12625        app.handle_action(Action::NextFilterFocus);
12626        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12627
12628        app.handle_action(Action::NextFilterFocus);
12629        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12630
12631        app.handle_action(Action::NextFilterFocus);
12632        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12633    }
12634
12635    #[test]
12636    fn test_cfn_input_focus_prev() {
12637        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12638        let mut app = test_app();
12639        app.current_service = Service::CloudFormationStacks;
12640        app.mode = Mode::FilterInput;
12641        app.cfn_state.input_focus = InputFocus::Filter;
12642
12643        app.handle_action(Action::PrevFilterFocus);
12644        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12645
12646        app.handle_action(Action::PrevFilterFocus);
12647        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12648
12649        app.handle_action(Action::PrevFilterFocus);
12650        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12651
12652        app.handle_action(Action::PrevFilterFocus);
12653        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12654    }
12655
12656    #[test]
12657    fn test_cw_logs_input_focus_prev() {
12658        let mut app = test_app();
12659        app.current_service = Service::CloudWatchLogGroups;
12660        app.mode = Mode::FilterInput;
12661        app.view_mode = ViewMode::Detail;
12662        app.log_groups_state.detail_tab = crate::ui::cw::logs::DetailTab::LogStreams;
12663        app.log_groups_state.input_focus = InputFocus::Filter;
12664
12665        app.handle_action(Action::PrevFilterFocus);
12666        assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
12667
12668        app.handle_action(Action::PrevFilterFocus);
12669        assert_eq!(
12670            app.log_groups_state.input_focus,
12671            InputFocus::Checkbox("ShowExpired")
12672        );
12673
12674        app.handle_action(Action::PrevFilterFocus);
12675        assert_eq!(
12676            app.log_groups_state.input_focus,
12677            InputFocus::Checkbox("ExactMatch")
12678        );
12679
12680        app.handle_action(Action::PrevFilterFocus);
12681        assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
12682    }
12683
12684    #[test]
12685    fn test_cw_events_input_focus_prev() {
12686        use crate::ui::cw::logs::EventFilterFocus;
12687        let mut app = test_app();
12688        app.mode = Mode::EventFilterInput;
12689        app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
12690
12691        app.handle_action(Action::PrevFilterFocus);
12692        assert_eq!(
12693            app.log_groups_state.event_input_focus,
12694            EventFilterFocus::DateRange
12695        );
12696
12697        app.handle_action(Action::PrevFilterFocus);
12698        assert_eq!(
12699            app.log_groups_state.event_input_focus,
12700            EventFilterFocus::Filter
12701        );
12702    }
12703
12704    #[test]
12705    fn test_cfn_input_focus_cycle_complete() {
12706        let mut app = test_app();
12707        app.current_service = Service::CloudFormationStacks;
12708        app.mode = Mode::FilterInput;
12709        app.cfn_state.input_focus = InputFocus::Filter;
12710
12711        // Cycle forward through all controls
12712        for _ in 0..4 {
12713            app.handle_action(Action::NextFilterFocus);
12714        }
12715        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12716
12717        // Cycle backward through all controls
12718        for _ in 0..4 {
12719            app.handle_action(Action::PrevFilterFocus);
12720        }
12721        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12722    }
12723
12724    #[test]
12725    fn test_cfn_filter_status_arrow_keys() {
12726        use crate::ui::cfn::{StatusFilter, STATUS_FILTER};
12727        let mut app = test_app();
12728        app.current_service = Service::CloudFormationStacks;
12729        app.mode = Mode::FilterInput;
12730        app.cfn_state.input_focus = STATUS_FILTER;
12731        app.cfn_state.status_filter = StatusFilter::All;
12732
12733        app.handle_action(Action::NextItem);
12734        assert_eq!(app.cfn_state.status_filter, StatusFilter::Active);
12735
12736        app.handle_action(Action::PrevItem);
12737        assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
12738    }
12739
12740    #[test]
12741    fn test_cfn_filter_shift_tab_cycles_backward() {
12742        use crate::ui::cfn::STATUS_FILTER;
12743        let mut app = test_app();
12744        app.current_service = Service::CloudFormationStacks;
12745        app.mode = Mode::FilterInput;
12746        app.cfn_state.input_focus = STATUS_FILTER;
12747
12748        // Shift+Tab should go backward
12749        app.handle_action(Action::PrevFilterFocus);
12750        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12751
12752        // From Input, Shift+Tab should wrap to Pagination
12753        app.handle_action(Action::PrevFilterFocus);
12754        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12755    }
12756
12757    #[test]
12758    fn test_cfn_pagination_arrow_keys() {
12759        let mut app = test_app();
12760        app.current_service = Service::CloudFormationStacks;
12761        app.mode = Mode::FilterInput;
12762        app.cfn_state.input_focus = InputFocus::Pagination;
12763        app.cfn_state.table.scroll_offset = 0;
12764        app.cfn_state.table.page_size = crate::common::PageSize::Ten;
12765
12766        // Add some test stacks
12767        app.cfn_state.table.items = (0..30)
12768            .map(|i| crate::cfn::Stack {
12769                name: format!("stack-{}", i),
12770                stack_id: format!("id-{}", i),
12771                status: "CREATE_COMPLETE".to_string(),
12772                created_time: "2024-01-01".to_string(),
12773                updated_time: String::new(),
12774                deleted_time: String::new(),
12775                drift_status: String::new(),
12776                last_drift_check_time: String::new(),
12777                status_reason: String::new(),
12778                description: String::new(),
12779                detailed_status: String::new(),
12780                root_stack: String::new(),
12781                parent_stack: String::new(),
12782                termination_protection: false,
12783                iam_role: String::new(),
12784                tags: Vec::new(),
12785                stack_policy: String::new(),
12786                rollback_monitoring_time: String::new(),
12787                rollback_alarms: Vec::new(),
12788                notification_arns: Vec::new(),
12789            })
12790            .collect();
12791
12792        // Right arrow should page forward
12793        app.handle_action(Action::PageDown);
12794        assert_eq!(app.cfn_state.table.scroll_offset, 10);
12795        // Verify page number calculation
12796        let page_size = app.cfn_state.table.page_size.value();
12797        let current_page = app.cfn_state.table.scroll_offset / page_size;
12798        assert_eq!(current_page, 1);
12799
12800        // Left arrow should page backward
12801        app.handle_action(Action::PageUp);
12802        assert_eq!(app.cfn_state.table.scroll_offset, 0);
12803        let current_page = app.cfn_state.table.scroll_offset / page_size;
12804        assert_eq!(current_page, 0);
12805    }
12806
12807    #[test]
12808    fn test_cfn_page_navigation_updates_selection() {
12809        let mut app = test_app();
12810        app.current_service = Service::CloudFormationStacks;
12811        app.mode = Mode::Normal;
12812
12813        // Add 30 test stacks
12814        app.cfn_state.table.items = (0..30)
12815            .map(|i| crate::cfn::Stack {
12816                name: format!("stack-{}", i),
12817                stack_id: format!("id-{}", i),
12818                status: "CREATE_COMPLETE".to_string(),
12819                created_time: "2024-01-01".to_string(),
12820                updated_time: String::new(),
12821                deleted_time: String::new(),
12822                drift_status: String::new(),
12823                last_drift_check_time: String::new(),
12824                status_reason: String::new(),
12825                description: String::new(),
12826                detailed_status: String::new(),
12827                root_stack: String::new(),
12828                parent_stack: String::new(),
12829                termination_protection: false,
12830                iam_role: String::new(),
12831                tags: Vec::new(),
12832                stack_policy: String::new(),
12833                rollback_monitoring_time: String::new(),
12834                rollback_alarms: Vec::new(),
12835                notification_arns: Vec::new(),
12836            })
12837            .collect();
12838
12839        app.cfn_state.table.reset();
12840        app.cfn_state.table.scroll_offset = 0;
12841
12842        // Page down should update selection
12843        app.handle_action(Action::PageDown);
12844        assert_eq!(app.cfn_state.table.selected, 10);
12845
12846        // Page down again
12847        app.handle_action(Action::PageDown);
12848        assert_eq!(app.cfn_state.table.selected, 20);
12849
12850        // Page up should update selection
12851        app.handle_action(Action::PageUp);
12852        assert_eq!(app.cfn_state.table.selected, 10);
12853    }
12854
12855    #[test]
12856    fn test_cfn_filter_input_only_when_focused() {
12857        use crate::ui::cfn::STATUS_FILTER;
12858        let mut app = test_app();
12859        app.current_service = Service::CloudFormationStacks;
12860        app.mode = Mode::FilterInput;
12861        app.cfn_state.input_focus = STATUS_FILTER;
12862        app.cfn_state.table.filter = String::new();
12863
12864        // Typing should not add to filter when focus is not on Input
12865        app.handle_action(Action::FilterInput('t'));
12866        app.handle_action(Action::FilterInput('e'));
12867        app.handle_action(Action::FilterInput('s'));
12868        app.handle_action(Action::FilterInput('t'));
12869        assert_eq!(app.cfn_state.table.filter, "");
12870
12871        // Switch to Input focus
12872        app.cfn_state.input_focus = InputFocus::Filter;
12873        app.handle_action(Action::FilterInput('t'));
12874        app.handle_action(Action::FilterInput('e'));
12875        app.handle_action(Action::FilterInput('s'));
12876        app.handle_action(Action::FilterInput('t'));
12877        assert_eq!(app.cfn_state.table.filter, "test");
12878    }
12879
12880    #[test]
12881    fn test_cfn_input_focus_resets_on_start() {
12882        let mut app = test_app();
12883        app.current_service = Service::CloudFormationStacks;
12884        app.service_selected = true;
12885        app.mode = Mode::Normal;
12886        app.cfn_state.input_focus = InputFocus::Pagination;
12887
12888        // Start filter should reset focus to Input
12889        app.handle_action(Action::StartFilter);
12890        assert_eq!(app.mode, Mode::FilterInput);
12891        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12892    }
12893
12894    #[test]
12895    fn test_iam_roles_input_focus_cycles_forward() {
12896        let mut app = test_app();
12897        app.current_service = Service::IamRoles;
12898        app.mode = Mode::FilterInput;
12899        app.iam_state.role_input_focus = InputFocus::Filter;
12900
12901        app.handle_action(Action::NextFilterFocus);
12902        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12903
12904        app.handle_action(Action::NextFilterFocus);
12905        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12906    }
12907
12908    #[test]
12909    fn test_iam_roles_input_focus_cycles_backward() {
12910        let mut app = test_app();
12911        app.current_service = Service::IamRoles;
12912        app.mode = Mode::FilterInput;
12913        app.iam_state.role_input_focus = InputFocus::Filter;
12914
12915        app.handle_action(Action::PrevFilterFocus);
12916        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12917
12918        app.handle_action(Action::PrevFilterFocus);
12919        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12920    }
12921
12922    #[test]
12923    fn test_iam_roles_filter_input_only_when_focused() {
12924        let mut app = test_app();
12925        app.current_service = Service::IamRoles;
12926        app.mode = Mode::FilterInput;
12927        app.iam_state.role_input_focus = InputFocus::Pagination;
12928        app.iam_state.roles.filter = String::new();
12929
12930        // Typing should not add to filter when focus is on Pagination
12931        app.handle_action(Action::FilterInput('t'));
12932        app.handle_action(Action::FilterInput('e'));
12933        app.handle_action(Action::FilterInput('s'));
12934        app.handle_action(Action::FilterInput('t'));
12935        assert_eq!(app.iam_state.roles.filter, "");
12936
12937        // Switch to Input focus
12938        app.iam_state.role_input_focus = InputFocus::Filter;
12939        app.handle_action(Action::FilterInput('t'));
12940        app.handle_action(Action::FilterInput('e'));
12941        app.handle_action(Action::FilterInput('s'));
12942        app.handle_action(Action::FilterInput('t'));
12943        assert_eq!(app.iam_state.roles.filter, "test");
12944    }
12945
12946    #[test]
12947    fn test_iam_roles_page_down_updates_scroll_offset() {
12948        let mut app = test_app();
12949        app.current_service = Service::IamRoles;
12950        app.mode = Mode::Normal;
12951        app.iam_state.roles.items = (0..50)
12952            .map(|i| crate::iam::IamRole {
12953                role_name: format!("role-{}", i),
12954                path: "/".to_string(),
12955                trusted_entities: "AWS Service".to_string(),
12956                last_activity: "N/A".to_string(),
12957                arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
12958                creation_time: "2024-01-01".to_string(),
12959                description: String::new(),
12960                max_session_duration: Some(3600),
12961            })
12962            .collect();
12963
12964        app.iam_state.roles.selected = 0;
12965        app.iam_state.roles.scroll_offset = 0;
12966
12967        // Page down should update both selected and scroll_offset
12968        app.handle_action(Action::PageDown);
12969        assert_eq!(app.iam_state.roles.selected, 10);
12970        // scroll_offset should be updated to keep selection visible
12971        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12972
12973        // Page down again
12974        app.handle_action(Action::PageDown);
12975        assert_eq!(app.iam_state.roles.selected, 20);
12976        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12977    }
12978
12979    #[test]
12980    fn test_application_selection_and_deployments_tab() {
12981        use crate::lambda::Application as LambdaApplication;
12982        use LambdaApplicationDetailTab;
12983
12984        let mut app = test_app();
12985        app.current_service = Service::LambdaApplications;
12986        app.service_selected = true;
12987        app.mode = Mode::Normal;
12988
12989        app.lambda_application_state.table.items = vec![LambdaApplication {
12990            name: "test-app".to_string(),
12991            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12992            description: "Test application".to_string(),
12993            status: "CREATE_COMPLETE".to_string(),
12994            last_modified: "2024-01-01".to_string(),
12995        }];
12996
12997        // Select application
12998        app.handle_action(Action::Select);
12999        assert_eq!(
13000            app.lambda_application_state.current_application,
13001            Some("test-app".to_string())
13002        );
13003        assert_eq!(
13004            app.lambda_application_state.detail_tab,
13005            LambdaApplicationDetailTab::Overview
13006        );
13007
13008        // Switch to Deployments tab
13009        app.handle_action(Action::NextDetailTab);
13010        assert_eq!(
13011            app.lambda_application_state.detail_tab,
13012            LambdaApplicationDetailTab::Deployments
13013        );
13014
13015        // Go back
13016        app.handle_action(Action::GoBack);
13017        assert_eq!(app.lambda_application_state.current_application, None);
13018    }
13019
13020    #[test]
13021    fn test_application_resources_filter_and_pagination() {
13022        use crate::lambda::Application as LambdaApplication;
13023        use LambdaApplicationDetailTab;
13024
13025        let mut app = test_app();
13026        app.current_service = Service::LambdaApplications;
13027        app.service_selected = true;
13028        app.mode = Mode::Normal;
13029
13030        app.lambda_application_state.table.items = vec![LambdaApplication {
13031            name: "test-app".to_string(),
13032            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13033            description: "Test application".to_string(),
13034            status: "CREATE_COMPLETE".to_string(),
13035            last_modified: "2024-01-01".to_string(),
13036        }];
13037
13038        // Select application
13039        app.handle_action(Action::Select);
13040        assert_eq!(
13041            app.lambda_application_state.detail_tab,
13042            LambdaApplicationDetailTab::Overview
13043        );
13044
13045        // Verify resources were loaded
13046        assert!(!app.lambda_application_state.resources.items.is_empty());
13047
13048        // Test filter focus cycling
13049        app.mode = Mode::FilterInput;
13050        assert_eq!(
13051            app.lambda_application_state.resource_input_focus,
13052            InputFocus::Filter
13053        );
13054
13055        app.handle_action(Action::NextFilterFocus);
13056        assert_eq!(
13057            app.lambda_application_state.resource_input_focus,
13058            InputFocus::Pagination
13059        );
13060
13061        app.handle_action(Action::PrevFilterFocus);
13062        assert_eq!(
13063            app.lambda_application_state.resource_input_focus,
13064            InputFocus::Filter
13065        );
13066    }
13067
13068    #[test]
13069    fn test_application_deployments_filter_and_pagination() {
13070        use crate::lambda::Application as LambdaApplication;
13071        use LambdaApplicationDetailTab;
13072
13073        let mut app = test_app();
13074        app.current_service = Service::LambdaApplications;
13075        app.service_selected = true;
13076        app.mode = Mode::Normal;
13077
13078        app.lambda_application_state.table.items = vec![LambdaApplication {
13079            name: "test-app".to_string(),
13080            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13081            description: "Test application".to_string(),
13082            status: "CREATE_COMPLETE".to_string(),
13083            last_modified: "2024-01-01".to_string(),
13084        }];
13085
13086        // Select application and switch to Deployments tab
13087        app.handle_action(Action::Select);
13088        app.handle_action(Action::NextDetailTab);
13089        assert_eq!(
13090            app.lambda_application_state.detail_tab,
13091            LambdaApplicationDetailTab::Deployments
13092        );
13093
13094        // Verify deployments were loaded
13095        assert!(!app.lambda_application_state.deployments.items.is_empty());
13096
13097        // Test filter focus cycling
13098        app.mode = Mode::FilterInput;
13099        assert_eq!(
13100            app.lambda_application_state.deployment_input_focus,
13101            InputFocus::Filter
13102        );
13103
13104        app.handle_action(Action::NextFilterFocus);
13105        assert_eq!(
13106            app.lambda_application_state.deployment_input_focus,
13107            InputFocus::Pagination
13108        );
13109
13110        app.handle_action(Action::PrevFilterFocus);
13111        assert_eq!(
13112            app.lambda_application_state.deployment_input_focus,
13113            InputFocus::Filter
13114        );
13115    }
13116
13117    #[test]
13118    fn test_application_resource_expansion() {
13119        use crate::lambda::Application as LambdaApplication;
13120        use LambdaApplicationDetailTab;
13121
13122        let mut app = test_app();
13123        app.current_service = Service::LambdaApplications;
13124        app.service_selected = true;
13125        app.mode = Mode::Normal;
13126
13127        app.lambda_application_state.table.items = vec![LambdaApplication {
13128            name: "test-app".to_string(),
13129            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13130            description: "Test application".to_string(),
13131            status: "CREATE_COMPLETE".to_string(),
13132            last_modified: "2024-01-01".to_string(),
13133        }];
13134
13135        // Select application (Overview tab by default)
13136        app.handle_action(Action::Select);
13137        assert_eq!(
13138            app.lambda_application_state.detail_tab,
13139            LambdaApplicationDetailTab::Overview
13140        );
13141
13142        // Expand resource
13143        app.handle_action(Action::NextPane);
13144        assert_eq!(
13145            app.lambda_application_state.resources.expanded_item,
13146            Some(0)
13147        );
13148
13149        // Collapse resource
13150        app.handle_action(Action::PrevPane);
13151        assert_eq!(app.lambda_application_state.resources.expanded_item, None);
13152    }
13153
13154    #[test]
13155    fn test_application_deployment_expansion() {
13156        use crate::lambda::Application as LambdaApplication;
13157        use LambdaApplicationDetailTab;
13158
13159        let mut app = test_app();
13160        app.current_service = Service::LambdaApplications;
13161        app.service_selected = true;
13162        app.mode = Mode::Normal;
13163
13164        app.lambda_application_state.table.items = vec![LambdaApplication {
13165            name: "test-app".to_string(),
13166            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13167            description: "Test application".to_string(),
13168            status: "CREATE_COMPLETE".to_string(),
13169            last_modified: "2024-01-01".to_string(),
13170        }];
13171
13172        // Select application and switch to Deployments tab
13173        app.handle_action(Action::Select);
13174        app.handle_action(Action::NextDetailTab);
13175        assert_eq!(
13176            app.lambda_application_state.detail_tab,
13177            LambdaApplicationDetailTab::Deployments
13178        );
13179
13180        // Expand deployment
13181        app.handle_action(Action::NextPane);
13182        assert_eq!(
13183            app.lambda_application_state.deployments.expanded_item,
13184            Some(0)
13185        );
13186
13187        // Collapse deployment
13188        app.handle_action(Action::PrevPane);
13189        assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
13190    }
13191
13192    #[test]
13193    fn test_s3_nested_prefix_expansion() {
13194        use crate::s3::Bucket;
13195        use crate::s3::Object as S3Object;
13196
13197        let mut app = test_app();
13198        app.current_service = Service::S3Buckets;
13199        app.service_selected = true;
13200        app.mode = Mode::Normal;
13201
13202        // Setup bucket with nested prefixes (2 levels)
13203        app.s3_state.buckets.items = vec![Bucket {
13204            name: "test-bucket".to_string(),
13205            region: "us-east-1".to_string(),
13206            creation_date: "2024-01-01".to_string(),
13207        }];
13208
13209        // Level 1: bucket preview
13210        app.s3_state.bucket_preview.insert(
13211            "test-bucket".to_string(),
13212            vec![S3Object {
13213                key: "level1/".to_string(),
13214                size: 0,
13215                last_modified: "".to_string(),
13216                is_prefix: true,
13217                storage_class: "".to_string(),
13218            }],
13219        );
13220
13221        // Level 2: nested prefix
13222        app.s3_state.prefix_preview.insert(
13223            "level1/".to_string(),
13224            vec![S3Object {
13225                key: "level1/level2/".to_string(),
13226                size: 0,
13227                last_modified: "".to_string(),
13228                is_prefix: true,
13229                storage_class: "".to_string(),
13230            }],
13231        );
13232
13233        // Expand bucket (row 0)
13234        app.s3_state.selected_row = 0;
13235        app.handle_action(Action::NextPane);
13236        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13237
13238        // Expand level1/ (row 1)
13239        app.s3_state.selected_row = 1;
13240        app.handle_action(Action::NextPane);
13241        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13242
13243        // Expand level2/ (row 2) - verifies nested expansion works
13244        app.s3_state.selected_row = 2;
13245        app.handle_action(Action::NextPane);
13246        assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
13247
13248        // Verify all are still expanded
13249        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13250        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13251    }
13252
13253    #[test]
13254    fn test_s3_nested_prefix_collapse() {
13255        use crate::s3::Bucket;
13256        use crate::s3::Object as S3Object;
13257
13258        let mut app = test_app();
13259        app.current_service = Service::S3Buckets;
13260        app.service_selected = true;
13261        app.mode = Mode::Normal;
13262
13263        app.s3_state.buckets.items = vec![Bucket {
13264            name: "test-bucket".to_string(),
13265            region: "us-east-1".to_string(),
13266            creation_date: "2024-01-01".to_string(),
13267        }];
13268
13269        app.s3_state.bucket_preview.insert(
13270            "test-bucket".to_string(),
13271            vec![S3Object {
13272                key: "level1/".to_string(),
13273                size: 0,
13274                last_modified: "".to_string(),
13275                is_prefix: true,
13276                storage_class: "".to_string(),
13277            }],
13278        );
13279
13280        app.s3_state.prefix_preview.insert(
13281            "level1/".to_string(),
13282            vec![S3Object {
13283                key: "level1/level2/".to_string(),
13284                size: 0,
13285                last_modified: "".to_string(),
13286                is_prefix: true,
13287                storage_class: "".to_string(),
13288            }],
13289        );
13290
13291        // Pre-expand all levels
13292        app.s3_state
13293            .expanded_prefixes
13294            .insert("test-bucket".to_string());
13295        app.s3_state.expanded_prefixes.insert("level1/".to_string());
13296        app.s3_state
13297            .expanded_prefixes
13298            .insert("level1/level2/".to_string());
13299
13300        // Collapse level2/ (row 2)
13301        app.s3_state.selected_row = 2;
13302        app.handle_action(Action::PrevPane);
13303        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
13304        assert!(app.s3_state.expanded_prefixes.contains("level1/")); // Parent still expanded
13305
13306        // Collapse level1/ (row 1)
13307        app.s3_state.selected_row = 1;
13308        app.handle_action(Action::PrevPane);
13309        assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
13310        assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); // Bucket still expanded
13311
13312        // Collapse bucket (row 0)
13313        app.s3_state.selected_row = 0;
13314        app.handle_action(Action::PrevPane);
13315        assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
13316    }
13317}
13318
13319#[cfg(test)]
13320mod sqs_tests {
13321    use super::*;
13322    use test_helpers::*;
13323
13324    #[test]
13325    fn test_sqs_filter_input() {
13326        let mut app = test_app();
13327        app.current_service = Service::SqsQueues;
13328        app.service_selected = true;
13329        app.mode = Mode::FilterInput;
13330
13331        app.handle_action(Action::FilterInput('t'));
13332        app.handle_action(Action::FilterInput('e'));
13333        app.handle_action(Action::FilterInput('s'));
13334        app.handle_action(Action::FilterInput('t'));
13335        assert_eq!(app.sqs_state.queues.filter, "test");
13336
13337        app.handle_action(Action::FilterBackspace);
13338        assert_eq!(app.sqs_state.queues.filter, "tes");
13339    }
13340
13341    #[test]
13342    fn test_sqs_start_filter() {
13343        let mut app = test_app();
13344        app.current_service = Service::SqsQueues;
13345        app.service_selected = true;
13346        app.mode = Mode::Normal;
13347
13348        app.handle_action(Action::StartFilter);
13349        assert_eq!(app.mode, Mode::FilterInput);
13350        assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13351    }
13352
13353    #[test]
13354    fn test_sqs_filter_focus_cycling() {
13355        let mut app = test_app();
13356        app.current_service = Service::SqsQueues;
13357        app.service_selected = true;
13358        app.mode = Mode::FilterInput;
13359        app.sqs_state.input_focus = InputFocus::Filter;
13360
13361        app.handle_action(Action::NextFilterFocus);
13362        assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13363
13364        app.handle_action(Action::NextFilterFocus);
13365        assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13366
13367        app.handle_action(Action::PrevFilterFocus);
13368        assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13369    }
13370
13371    #[test]
13372    fn test_sqs_navigation() {
13373        let mut app = test_app();
13374        app.current_service = Service::SqsQueues;
13375        app.service_selected = true;
13376        app.mode = Mode::Normal;
13377        app.sqs_state.queues.items = (0..10)
13378            .map(|i| crate::sqs::Queue {
13379                name: format!("queue{}", i),
13380                url: String::new(),
13381                queue_type: "Standard".to_string(),
13382                created_timestamp: String::new(),
13383                messages_available: "0".to_string(),
13384                messages_in_flight: "0".to_string(),
13385                encryption: "Disabled".to_string(),
13386                content_based_deduplication: "Disabled".to_string(),
13387                last_modified_timestamp: String::new(),
13388                visibility_timeout: String::new(),
13389                message_retention_period: String::new(),
13390                maximum_message_size: String::new(),
13391                delivery_delay: String::new(),
13392                receive_message_wait_time: String::new(),
13393                high_throughput_fifo: "N/A".to_string(),
13394                deduplication_scope: "N/A".to_string(),
13395                fifo_throughput_limit: "N/A".to_string(),
13396                dead_letter_queue: "-".to_string(),
13397                messages_delayed: "0".to_string(),
13398                redrive_allow_policy: "-".to_string(),
13399                redrive_policy: "".to_string(),
13400                redrive_task_id: "-".to_string(),
13401                redrive_task_start_time: "-".to_string(),
13402                redrive_task_status: "-".to_string(),
13403                redrive_task_percent: "-".to_string(),
13404                redrive_task_destination: "-".to_string(),
13405            })
13406            .collect();
13407
13408        app.handle_action(Action::NextItem);
13409        assert_eq!(app.sqs_state.queues.selected, 1);
13410
13411        app.handle_action(Action::PrevItem);
13412        assert_eq!(app.sqs_state.queues.selected, 0);
13413    }
13414
13415    #[test]
13416    fn test_sqs_page_navigation() {
13417        let mut app = test_app();
13418        app.current_service = Service::SqsQueues;
13419        app.service_selected = true;
13420        app.mode = Mode::Normal;
13421        app.sqs_state.queues.items = (0..100)
13422            .map(|i| crate::sqs::Queue {
13423                name: format!("queue{}", i),
13424                url: String::new(),
13425                queue_type: "Standard".to_string(),
13426                created_timestamp: String::new(),
13427                messages_available: "0".to_string(),
13428                messages_in_flight: "0".to_string(),
13429                encryption: "Disabled".to_string(),
13430                content_based_deduplication: "Disabled".to_string(),
13431                last_modified_timestamp: String::new(),
13432                visibility_timeout: String::new(),
13433                message_retention_period: String::new(),
13434                maximum_message_size: String::new(),
13435                delivery_delay: String::new(),
13436                receive_message_wait_time: String::new(),
13437                high_throughput_fifo: "N/A".to_string(),
13438                deduplication_scope: "N/A".to_string(),
13439                fifo_throughput_limit: "N/A".to_string(),
13440                dead_letter_queue: "-".to_string(),
13441                messages_delayed: "0".to_string(),
13442                redrive_allow_policy: "-".to_string(),
13443                redrive_policy: "".to_string(),
13444                redrive_task_id: "-".to_string(),
13445                redrive_task_start_time: "-".to_string(),
13446                redrive_task_status: "-".to_string(),
13447                redrive_task_percent: "-".to_string(),
13448                redrive_task_destination: "-".to_string(),
13449            })
13450            .collect();
13451
13452        app.handle_action(Action::PageDown);
13453        assert_eq!(app.sqs_state.queues.selected, 10);
13454
13455        app.handle_action(Action::PageUp);
13456        assert_eq!(app.sqs_state.queues.selected, 0);
13457    }
13458
13459    #[test]
13460    fn test_sqs_queue_expansion() {
13461        let mut app = test_app();
13462        app.current_service = Service::SqsQueues;
13463        app.service_selected = true;
13464        app.sqs_state.queues.items = vec![crate::sqs::Queue {
13465            name: "my-queue".to_string(),
13466            url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13467            queue_type: "Standard".to_string(),
13468            created_timestamp: "2023-01-01".to_string(),
13469            messages_available: "5".to_string(),
13470            messages_in_flight: "2".to_string(),
13471            encryption: "Enabled".to_string(),
13472            content_based_deduplication: "Disabled".to_string(),
13473            last_modified_timestamp: "2023-01-02".to_string(),
13474            visibility_timeout: "30".to_string(),
13475            message_retention_period: "345600".to_string(),
13476            maximum_message_size: "262144".to_string(),
13477            delivery_delay: "0".to_string(),
13478            receive_message_wait_time: "0".to_string(),
13479            high_throughput_fifo: "N/A".to_string(),
13480            deduplication_scope: "N/A".to_string(),
13481            fifo_throughput_limit: "N/A".to_string(),
13482            dead_letter_queue: "-".to_string(),
13483            messages_delayed: "0".to_string(),
13484            redrive_allow_policy: "-".to_string(),
13485            redrive_policy: "".to_string(),
13486            redrive_task_id: "-".to_string(),
13487            redrive_task_start_time: "-".to_string(),
13488            redrive_task_status: "-".to_string(),
13489            redrive_task_percent: "-".to_string(),
13490            redrive_task_destination: "-".to_string(),
13491        }];
13492        app.sqs_state.queues.selected = 0;
13493
13494        assert_eq!(app.sqs_state.queues.expanded_item, None);
13495
13496        // Right arrow expands
13497        app.handle_action(Action::NextPane);
13498        assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13499
13500        // Right arrow again keeps it expanded
13501        app.handle_action(Action::NextPane);
13502        assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13503
13504        // Left arrow collapses
13505        app.handle_action(Action::PrevPane);
13506        assert_eq!(app.sqs_state.queues.expanded_item, None);
13507
13508        // Left arrow again keeps it collapsed
13509        app.handle_action(Action::PrevPane);
13510        assert_eq!(app.sqs_state.queues.expanded_item, None);
13511    }
13512
13513    #[test]
13514    fn test_sqs_column_toggle() {
13515        use crate::sqs::queue::Column as SqsColumn;
13516        let mut app = test_app();
13517        app.current_service = Service::SqsQueues;
13518        app.service_selected = true;
13519        app.mode = Mode::ColumnSelector;
13520
13521        // Start with all columns visible
13522        app.sqs_visible_column_ids = SqsColumn::ids();
13523        let initial_count = app.sqs_visible_column_ids.len();
13524
13525        // Select first column (index 0) and toggle it
13526        app.column_selector_index = 0;
13527        app.handle_action(Action::ToggleColumn);
13528
13529        // First column should be removed
13530        assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
13531        assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13532
13533        // Toggle it back
13534        app.handle_action(Action::ToggleColumn);
13535        assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
13536        assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13537    }
13538
13539    #[test]
13540    fn test_sqs_column_selector_navigation() {
13541        let mut app = test_app();
13542        app.current_service = Service::SqsQueues;
13543        app.service_selected = true;
13544        app.mode = Mode::ColumnSelector;
13545        app.column_selector_index = 0;
13546
13547        // Should be able to navigate through all columns
13548        let max_index = app.sqs_column_ids.len() - 1;
13549
13550        // Navigate to last column
13551        for _ in 0..max_index {
13552            app.handle_action(Action::NextItem);
13553        }
13554        assert_eq!(app.column_selector_index, max_index);
13555
13556        // Navigate back to first
13557        for _ in 0..max_index {
13558            app.handle_action(Action::PrevItem);
13559        }
13560        assert_eq!(app.column_selector_index, 0);
13561    }
13562
13563    #[test]
13564    fn test_sqs_queue_selection() {
13565        let mut app = test_app();
13566        app.current_service = Service::SqsQueues;
13567        app.service_selected = true;
13568        app.mode = Mode::Normal;
13569        app.sqs_state.queues.items = vec![crate::sqs::Queue {
13570            name: "my-queue".to_string(),
13571            url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13572            queue_type: "Standard".to_string(),
13573            created_timestamp: "2023-01-01".to_string(),
13574            messages_available: "5".to_string(),
13575            messages_in_flight: "2".to_string(),
13576            encryption: "Enabled".to_string(),
13577            content_based_deduplication: "Disabled".to_string(),
13578            last_modified_timestamp: "2023-01-02".to_string(),
13579            visibility_timeout: "30".to_string(),
13580            message_retention_period: "345600".to_string(),
13581            maximum_message_size: "262144".to_string(),
13582            delivery_delay: "0".to_string(),
13583            receive_message_wait_time: "0".to_string(),
13584            high_throughput_fifo: "N/A".to_string(),
13585            deduplication_scope: "N/A".to_string(),
13586            fifo_throughput_limit: "N/A".to_string(),
13587            dead_letter_queue: "-".to_string(),
13588            messages_delayed: "0".to_string(),
13589            redrive_allow_policy: "-".to_string(),
13590            redrive_policy: "".to_string(),
13591            redrive_task_id: "-".to_string(),
13592            redrive_task_start_time: "-".to_string(),
13593            redrive_task_status: "-".to_string(),
13594            redrive_task_percent: "-".to_string(),
13595            redrive_task_destination: "-".to_string(),
13596        }];
13597        app.sqs_state.queues.selected = 0;
13598
13599        assert_eq!(app.sqs_state.current_queue, None);
13600
13601        // Select queue
13602        app.handle_action(Action::Select);
13603        assert_eq!(
13604            app.sqs_state.current_queue,
13605            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
13606        );
13607
13608        // Go back
13609        app.handle_action(Action::GoBack);
13610        assert_eq!(app.sqs_state.current_queue, None);
13611    }
13612
13613    #[test]
13614    fn test_sqs_lambda_triggers_expand_collapse() {
13615        let mut app = test_app();
13616        app.current_service = Service::SqsQueues;
13617        app.service_selected = true;
13618        app.sqs_state.current_queue =
13619            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13620        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13621        app.sqs_state.triggers.items = vec![crate::sqs::LambdaTrigger {
13622            uuid: "test-uuid".to_string(),
13623            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13624            status: "Enabled".to_string(),
13625            last_modified: "2024-01-01T00:00:00Z".to_string(),
13626        }];
13627        app.sqs_state.triggers.selected = 0;
13628
13629        assert_eq!(app.sqs_state.triggers.expanded_item, None);
13630
13631        // Right arrow expands
13632        app.handle_action(Action::NextPane);
13633        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13634
13635        // Left arrow collapses
13636        app.handle_action(Action::PrevPane);
13637        assert_eq!(app.sqs_state.triggers.expanded_item, None);
13638    }
13639
13640    #[test]
13641    fn test_sqs_lambda_triggers_expand_toggle() {
13642        let mut app = test_app();
13643        app.current_service = Service::SqsQueues;
13644        app.service_selected = true;
13645        app.sqs_state.current_queue =
13646            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13647        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13648        app.sqs_state.triggers.items = vec![crate::sqs::LambdaTrigger {
13649            uuid: "test-uuid".to_string(),
13650            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13651            status: "Enabled".to_string(),
13652            last_modified: "2024-01-01T00:00:00Z".to_string(),
13653        }];
13654        app.sqs_state.triggers.selected = 0;
13655
13656        // Expand
13657        app.handle_action(Action::NextPane);
13658        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13659
13660        // Toggle collapses
13661        app.handle_action(Action::NextPane);
13662        assert_eq!(app.sqs_state.triggers.expanded_item, None);
13663
13664        // Toggle expands again
13665        app.handle_action(Action::NextPane);
13666        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13667    }
13668
13669    #[test]
13670    fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
13671        use crate::ui::sqs::filtered_lambda_triggers;
13672
13673        let mut app = test_app();
13674        app.current_service = Service::SqsQueues;
13675        app.service_selected = true;
13676        app.sqs_state.current_queue =
13677            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13678        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13679        app.sqs_state.triggers.items = vec![
13680            crate::sqs::LambdaTrigger {
13681                uuid: "uuid-3".to_string(),
13682                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
13683                status: "Enabled".to_string(),
13684                last_modified: "2024-03-01T00:00:00Z".to_string(),
13685            },
13686            crate::sqs::LambdaTrigger {
13687                uuid: "uuid-1".to_string(),
13688                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
13689                status: "Enabled".to_string(),
13690                last_modified: "2024-01-01T00:00:00Z".to_string(),
13691            },
13692            crate::sqs::LambdaTrigger {
13693                uuid: "uuid-2".to_string(),
13694                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
13695                status: "Enabled".to_string(),
13696                last_modified: "2024-02-01T00:00:00Z".to_string(),
13697            },
13698        ];
13699
13700        let sorted = filtered_lambda_triggers(&app);
13701
13702        // Should be sorted by last_modified ASC
13703        assert_eq!(sorted.len(), 3);
13704        assert_eq!(sorted[0].uuid, "uuid-1");
13705        assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
13706        assert_eq!(sorted[1].uuid, "uuid-2");
13707        assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
13708        assert_eq!(sorted[2].uuid, "uuid-3");
13709        assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
13710    }
13711
13712    #[test]
13713    fn test_sqs_lambda_triggers_filter_input() {
13714        let mut app = test_app();
13715        app.current_service = Service::SqsQueues;
13716        app.service_selected = true;
13717        app.mode = Mode::FilterInput;
13718        app.sqs_state.current_queue =
13719            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13720        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13721        app.sqs_state.input_focus = InputFocus::Filter;
13722
13723        assert_eq!(app.sqs_state.triggers.filter, "");
13724
13725        // Type characters
13726        app.handle_action(Action::FilterInput('t'));
13727        assert_eq!(app.sqs_state.triggers.filter, "t");
13728
13729        app.handle_action(Action::FilterInput('e'));
13730        assert_eq!(app.sqs_state.triggers.filter, "te");
13731
13732        app.handle_action(Action::FilterInput('s'));
13733        assert_eq!(app.sqs_state.triggers.filter, "tes");
13734
13735        app.handle_action(Action::FilterInput('t'));
13736        assert_eq!(app.sqs_state.triggers.filter, "test");
13737
13738        // Backspace
13739        app.handle_action(Action::FilterBackspace);
13740        assert_eq!(app.sqs_state.triggers.filter, "tes");
13741    }
13742
13743    #[test]
13744    fn test_sqs_lambda_triggers_filter_applied() {
13745        use crate::ui::sqs::filtered_lambda_triggers;
13746
13747        let mut app = test_app();
13748        app.current_service = Service::SqsQueues;
13749        app.service_selected = true;
13750        app.sqs_state.current_queue =
13751            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13752        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13753        app.sqs_state.triggers.items = vec![
13754            crate::sqs::LambdaTrigger {
13755                uuid: "uuid-1".to_string(),
13756                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
13757                status: "Enabled".to_string(),
13758                last_modified: "2024-01-01T00:00:00Z".to_string(),
13759            },
13760            crate::sqs::LambdaTrigger {
13761                uuid: "uuid-2".to_string(),
13762                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
13763                status: "Enabled".to_string(),
13764                last_modified: "2024-02-01T00:00:00Z".to_string(),
13765            },
13766            crate::sqs::LambdaTrigger {
13767                uuid: "uuid-3".to_string(),
13768                arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
13769                status: "Enabled".to_string(),
13770                last_modified: "2024-03-01T00:00:00Z".to_string(),
13771            },
13772        ];
13773
13774        // No filter - all items
13775        let filtered = filtered_lambda_triggers(&app);
13776        assert_eq!(filtered.len(), 3);
13777
13778        // Filter by "alpha"
13779        app.sqs_state.triggers.filter = "alpha".to_string();
13780        let filtered = filtered_lambda_triggers(&app);
13781        assert_eq!(filtered.len(), 1);
13782        assert_eq!(
13783            filtered[0].arn,
13784            "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13785        );
13786
13787        // Filter by "test" - matches 2
13788        app.sqs_state.triggers.filter = "test".to_string();
13789        let filtered = filtered_lambda_triggers(&app);
13790        assert_eq!(filtered.len(), 2);
13791        assert_eq!(
13792            filtered[0].arn,
13793            "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13794        );
13795        assert_eq!(
13796            filtered[1].arn,
13797            "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
13798        );
13799
13800        // Filter by uuid
13801        app.sqs_state.triggers.filter = "uuid-3".to_string();
13802        let filtered = filtered_lambda_triggers(&app);
13803        assert_eq!(filtered.len(), 1);
13804        assert_eq!(filtered[0].uuid, "uuid-3");
13805    }
13806
13807    #[test]
13808    fn test_sqs_triggers_navigation() {
13809        let mut app = test_app();
13810        app.service_selected = true;
13811        app.mode = Mode::Normal;
13812        app.current_service = Service::SqsQueues;
13813        app.sqs_state.current_queue = Some("test-queue".to_string());
13814        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13815        app.sqs_state.triggers.items = vec![
13816            crate::sqs::LambdaTrigger {
13817                uuid: "1".to_string(),
13818                arn: "arn1".to_string(),
13819                status: "Enabled".to_string(),
13820                last_modified: "2024-01-01".to_string(),
13821            },
13822            crate::sqs::LambdaTrigger {
13823                uuid: "2".to_string(),
13824                arn: "arn2".to_string(),
13825                status: "Enabled".to_string(),
13826                last_modified: "2024-01-02".to_string(),
13827            },
13828        ];
13829
13830        assert_eq!(app.sqs_state.triggers.selected, 0);
13831        app.next_item();
13832        assert_eq!(app.sqs_state.triggers.selected, 1);
13833        app.prev_item();
13834        assert_eq!(app.sqs_state.triggers.selected, 0);
13835    }
13836
13837    #[test]
13838    fn test_sqs_pipes_navigation() {
13839        let mut app = test_app();
13840        app.service_selected = true;
13841        app.mode = Mode::Normal;
13842        app.current_service = Service::SqsQueues;
13843        app.sqs_state.current_queue = Some("test-queue".to_string());
13844        app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
13845        app.sqs_state.pipes.items = vec![
13846            crate::sqs::EventBridgePipe {
13847                name: "pipe1".to_string(),
13848                status: "RUNNING".to_string(),
13849                target: "target1".to_string(),
13850                last_modified: "2024-01-01".to_string(),
13851            },
13852            crate::sqs::EventBridgePipe {
13853                name: "pipe2".to_string(),
13854                status: "RUNNING".to_string(),
13855                target: "target2".to_string(),
13856                last_modified: "2024-01-02".to_string(),
13857            },
13858        ];
13859
13860        assert_eq!(app.sqs_state.pipes.selected, 0);
13861        app.next_item();
13862        assert_eq!(app.sqs_state.pipes.selected, 1);
13863        app.prev_item();
13864        assert_eq!(app.sqs_state.pipes.selected, 0);
13865    }
13866
13867    #[test]
13868    fn test_sqs_tags_navigation() {
13869        let mut app = test_app();
13870        app.service_selected = true;
13871        app.mode = Mode::Normal;
13872        app.current_service = Service::SqsQueues;
13873        app.sqs_state.current_queue = Some("test-queue".to_string());
13874        app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
13875        app.sqs_state.tags.items = vec![
13876            crate::sqs::QueueTag {
13877                key: "Env".to_string(),
13878                value: "prod".to_string(),
13879            },
13880            crate::sqs::QueueTag {
13881                key: "Team".to_string(),
13882                value: "backend".to_string(),
13883            },
13884        ];
13885
13886        assert_eq!(app.sqs_state.tags.selected, 0);
13887        app.next_item();
13888        assert_eq!(app.sqs_state.tags.selected, 1);
13889        app.prev_item();
13890        assert_eq!(app.sqs_state.tags.selected, 0);
13891    }
13892
13893    #[test]
13894    fn test_sqs_queues_navigation() {
13895        let mut app = test_app();
13896        app.service_selected = true;
13897        app.mode = Mode::Normal;
13898        app.current_service = Service::SqsQueues;
13899        app.sqs_state.queues.items = vec![
13900            crate::sqs::Queue {
13901                name: "queue1".to_string(),
13902                url: "url1".to_string(),
13903                queue_type: "Standard".to_string(),
13904                created_timestamp: "".to_string(),
13905                messages_available: "0".to_string(),
13906                messages_in_flight: "0".to_string(),
13907                encryption: "Disabled".to_string(),
13908                content_based_deduplication: "Disabled".to_string(),
13909                last_modified_timestamp: "".to_string(),
13910                visibility_timeout: "".to_string(),
13911                message_retention_period: "".to_string(),
13912                maximum_message_size: "".to_string(),
13913                delivery_delay: "".to_string(),
13914                receive_message_wait_time: "".to_string(),
13915                high_throughput_fifo: "-".to_string(),
13916                deduplication_scope: "-".to_string(),
13917                fifo_throughput_limit: "-".to_string(),
13918                dead_letter_queue: "-".to_string(),
13919                messages_delayed: "0".to_string(),
13920                redrive_allow_policy: "-".to_string(),
13921                redrive_policy: "".to_string(),
13922                redrive_task_id: "-".to_string(),
13923                redrive_task_start_time: "-".to_string(),
13924                redrive_task_status: "-".to_string(),
13925                redrive_task_percent: "-".to_string(),
13926                redrive_task_destination: "-".to_string(),
13927            },
13928            crate::sqs::Queue {
13929                name: "queue2".to_string(),
13930                url: "url2".to_string(),
13931                queue_type: "Standard".to_string(),
13932                created_timestamp: "".to_string(),
13933                messages_available: "0".to_string(),
13934                messages_in_flight: "0".to_string(),
13935                encryption: "Disabled".to_string(),
13936                content_based_deduplication: "Disabled".to_string(),
13937                last_modified_timestamp: "".to_string(),
13938                visibility_timeout: "".to_string(),
13939                message_retention_period: "".to_string(),
13940                maximum_message_size: "".to_string(),
13941                delivery_delay: "".to_string(),
13942                receive_message_wait_time: "".to_string(),
13943                high_throughput_fifo: "-".to_string(),
13944                deduplication_scope: "-".to_string(),
13945                fifo_throughput_limit: "-".to_string(),
13946                dead_letter_queue: "-".to_string(),
13947                messages_delayed: "0".to_string(),
13948                redrive_allow_policy: "-".to_string(),
13949                redrive_policy: "".to_string(),
13950                redrive_task_id: "-".to_string(),
13951                redrive_task_start_time: "-".to_string(),
13952                redrive_task_status: "-".to_string(),
13953                redrive_task_percent: "-".to_string(),
13954                redrive_task_destination: "-".to_string(),
13955            },
13956        ];
13957
13958        assert_eq!(app.sqs_state.queues.selected, 0);
13959        app.next_item();
13960        assert_eq!(app.sqs_state.queues.selected, 1);
13961        app.prev_item();
13962        assert_eq!(app.sqs_state.queues.selected, 0);
13963    }
13964
13965    #[test]
13966    fn test_sqs_subscriptions_navigation() {
13967        let mut app = test_app();
13968        app.service_selected = true;
13969        app.mode = Mode::Normal;
13970        app.current_service = Service::SqsQueues;
13971        app.sqs_state.current_queue = Some("test-queue".to_string());
13972        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
13973        app.sqs_state.subscriptions.items = vec![
13974            crate::sqs::SnsSubscription {
13975                subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
13976                topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
13977            },
13978            crate::sqs::SnsSubscription {
13979                subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
13980                topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
13981            },
13982        ];
13983
13984        assert_eq!(app.sqs_state.subscriptions.selected, 0);
13985        app.next_item();
13986        assert_eq!(app.sqs_state.subscriptions.selected, 1);
13987        app.prev_item();
13988        assert_eq!(app.sqs_state.subscriptions.selected, 0);
13989    }
13990
13991    #[test]
13992    fn test_sqs_subscription_region_dropdown_navigation() {
13993        let mut app = test_app();
13994        app.service_selected = true;
13995        app.mode = Mode::FilterInput;
13996        app.current_service = Service::SqsQueues;
13997        app.sqs_state.current_queue = Some("test-queue".to_string());
13998        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
13999        app.sqs_state.input_focus = crate::common::InputFocus::Dropdown("SubscriptionRegion");
14000
14001        assert_eq!(app.sqs_state.subscription_region_selected, 0);
14002        app.next_item();
14003        assert_eq!(app.sqs_state.subscription_region_selected, 1);
14004        app.next_item();
14005        assert_eq!(app.sqs_state.subscription_region_selected, 2);
14006        app.prev_item();
14007        assert_eq!(app.sqs_state.subscription_region_selected, 1);
14008        app.prev_item();
14009        assert_eq!(app.sqs_state.subscription_region_selected, 0);
14010    }
14011
14012    #[test]
14013    fn test_sqs_subscription_region_selection() {
14014        let mut app = test_app();
14015        app.service_selected = true;
14016        app.mode = Mode::FilterInput;
14017        app.current_service = Service::SqsQueues;
14018        app.sqs_state.current_queue = Some("test-queue".to_string());
14019        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14020        app.sqs_state.input_focus = crate::common::InputFocus::Dropdown("SubscriptionRegion");
14021        app.sqs_state.subscription_region_selected = 2; // us-west-1
14022
14023        assert_eq!(app.sqs_state.subscription_region_filter, "");
14024        app.handle_action(Action::ApplyFilter);
14025        assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
14026        assert_eq!(app.mode, Mode::Normal);
14027    }
14028
14029    #[test]
14030    fn test_s3_object_filter_resets_selection() {
14031        let mut app = test_app();
14032        app.service_selected = true;
14033        app.current_service = Service::S3Buckets;
14034        app.s3_state.current_bucket = Some("test-bucket".to_string());
14035        app.s3_state.selected_row = 5;
14036        app.mode = Mode::FilterInput;
14037
14038        app.handle_action(Action::CloseMenu);
14039
14040        assert_eq!(app.s3_state.selected_row, 0);
14041        assert_eq!(app.mode, Mode::Normal);
14042    }
14043
14044    #[test]
14045    fn test_s3_bucket_filter_resets_selection() {
14046        let mut app = test_app();
14047        app.service_selected = true;
14048        app.current_service = Service::S3Buckets;
14049        app.s3_state.selected_row = 10;
14050        app.mode = Mode::FilterInput;
14051
14052        app.handle_action(Action::CloseMenu);
14053
14054        assert_eq!(app.s3_state.selected_row, 0);
14055        assert_eq!(app.mode, Mode::Normal);
14056    }
14057
14058    #[test]
14059    fn test_s3_selection_stays_in_bounds() {
14060        let mut app = test_app();
14061        app.service_selected = true;
14062        app.current_service = Service::S3Buckets;
14063        app.s3_state.selected_row = 0;
14064        app.s3_state.selected_object = 0;
14065
14066        // Simulate going up from row 0
14067        app.prev_item();
14068
14069        // Should stay at 0, not wrap to negative
14070        assert_eq!(app.s3_state.selected_row, 0);
14071        assert_eq!(app.s3_state.selected_object, 0);
14072    }
14073}